In this blog post, I'll show you how to install Docker and a Docker registry server on Ubuntu.
I'll run all steps as the root user. If you are not root, prepend sudo to the commands or start an interactive root shell with sudo -i.
Docker ¶
First, update all installed packages.
apt update
apt full-upgrade
Install Docker from Docker's official apt repository:
apt install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl start docker
systemctl enable docker
Test the installation with:
docker run --rm hello-world
You should see a hello message from Docker. The option --rm automatically deletes the container after it exits.
Firewall ¶
On servers connected to the Internet, I usually install a firewall. On Ubuntu systems, I use UFW for this purpose. On this installation, UFW is already installed (VPS server from Hetzner (referral link)). If it's not installed on your server, install it with apt install ufw.
I usually change the SSH daemon's default port 22 to another random port (/etc/ssh/sshd_config). In this installation, it's 35353. Open the port for SSH in the firewall.
ufw allow 35353/tcp
ufw enable
Double-check the port before you enable UFW.
Check the firewall status:
ufw status
Docker Registry ¶
A Docker registry is like a Git repository where you can push images to and pull images from. The Docker registry server is a Docker container that we can start with this command:
mkdir /var/lib/docker-registry
docker run -d -p 5000:5000 -v /var/lib/docker-registry:/var/lib/registry --restart=always --name registry registry:2
Container port 5000 is mapped to local port 5000, and with --restart=always we ensure that the container starts when the host system boots up.
-v bind-mounts the host directory /var/lib/docker-registry into the registry container at /var/lib/registry.
We are using the standard settings of the Docker registry server here.
UFW / Docker Issue ¶
At this point, we need to fix the UFW/Docker issue. Docker and UFW both create iptables rules. When you publish a container port with -p, Docker adds rules that can bypass UFW for published ports.
In this installation, we started the Docker registry server on port 5000, and this port is now reachable from the public network. You can verify that with nmap from another computer:
nmap <ip_address>
The output of nmap shows open ports:
PORT STATE SERVICE
5000/tcp open upnp
Note that port 35353 does not appear in this list because nmap scans only the most common 1,000 ports by default. Use -p- to scan all 65,535 ports.
We'll use the actively maintained utility from this repository to apply the fix:
https://github.com/chaifeng/ufw-docker
Install the utility:
wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
chmod +x /usr/local/bin/ufw-docker
Preview and install the UFW changes:
ufw-docker check
ufw-docker install --docker-subnets
ufw reload
ufw-docker install --docker-subnets updates UFW so that Docker keeps its normal networking behavior, while published ports are no longer exposed publicly by default.
Run nmap again. Port 5000 should no longer be accessible.
If you want to expose a specific container port later, use ufw-docker allow. Example:
ufw-docker allow registry 5000/tcp
Remove that rule again with:
ufw-docker delete allow registry 5000/tcp
For details, see the ufw-docker utility documentation.
Reverse proxy ¶
Now we can no longer connect to the Docker registry server directly from the outside. However, the purpose of a registry is to pull and push images from other computers. We could open the port in UFW, but for this demo installation, I want to use Nginx as a reverse proxy.
It is worth noting that exposing a registry without authentication is risky. In this setup, we keep the registry port protected by firewall rules and expose access through Nginx with Basic Authentication and TLS.
First, I added A and AAAA records to my DNS configuration. For this installation, I use the domain docker-registry.ralscha.ch. A domain is needed if you want to request a TLS certificate from Let's Encrypt.
Install Nginx and certbot:
apt install nginx
apt install python3-certbot-nginx
Open ports 80 and 443 in the UFW firewall:
ufw allow 80/tcp
ufw allow 443/tcp
We protect access to the registry with Basic Authentication and need to create a password file for Nginx. We can run the registry image to create this file. Here with user demouser and password mysupersecretpassword.
docker run --rm --entrypoint htpasswd registry:2 -Bbn demouser mysupersecretpassword > /etc/nginx/nginx.htpasswd
Note that this only protects external access to the registry. As mentioned before, somebody with shell access on the server could still push images if they can reach the local registry endpoint.
Create the Nginx configuration file:
cd /etc/nginx/sites-available/
rm default
rm ../sites-enabled/default
nano registry
Paste the following configuration into the file:
server {
listen 80;
listen [::]:80;
server_name docker-registry.ralscha.ch;
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
auth_basic "Docker Registry Realm";
auth_basic_user_file /etc/nginx/nginx.htpasswd;
proxy_pass http://localhost:5000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
}
Check out this site for more information:
https://docs.docker.com/registry/
Enable the site and restart Nginx:
ln -s /etc/nginx/sites-available/registry /etc/nginx/sites-enabled/registry
systemctl restart nginx
In the configuration above, we only configured HTTP (port 80). The next step is to create a TLS certificate and configure HTTPS (port 443). The certbot command-line tool conveniently performs both tasks.
certbot --nginx
When you run this command for the first time, it asks you about your email, which is used for urgent renewal and security notices. The command then lists all configured Nginx sites, in this case, only one.
Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: docker-registry.ralscha.ch
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel):
Select the site for which you want to create the TLS certificate.
Certbot acquires the TLS certificate and updates the Nginx configuration file.
Using Docker registry ¶
We can now use our self-hosted Docker registry.
I'm testing this with a Quarkus starter application. I run the following commands on my development computer.
mvn io.quarkus.platform:quarkus-maven-plugin:create -DprojectGroupId=ch.ralscha -DprojectArtifactId=test -DclassName="ch.ralscha.test.GreetingResource" -Dpath="/hello"
cd test
./mvnw quarkus:add-extension -Dextensions="container-image-docker"
./mvnw clean package -Dquarkus.container-image.build=true
On my computer, this creates an image with the name sr/test:1.0-SNAPSHOT. Before you push images to a remote registry, you need to tag them correctly. The tag must start with the registry server address (docker-registry.ralscha.ch/).
docker tag sr/test:1.0-SNAPSHOT docker-registry.ralscha.ch/test
docker login docker-registry.ralscha.ch
docker push docker-registry.ralscha.ch/test
The login command asks for the credentials. Enter demouser and mysupersecretpassword. These are the username and password we configured in the previous step.
It's important to note that docker login stores your credentials in plaintext in a file by default. Check out this documentation on how to configure a more secure solution.
The push command uploads the image from your local Docker installation to the remote registry.
You can check the stored images in the registry with this command.
curl --user demouser:mysupersecretpassword https://docker-registry.ralscha.ch/v2/_catalog
The Docker registry server provides a REST API. See this page for more information.
For testing purposes, you can now pull the image. Delete the images in your local Docker installation, pull the image from the remote registry, and run it.
docker image remove sr/test:1.0-SNAPSHOT
docker image remove docker-registry.ralscha.ch/test
docker pull docker-registry.ralscha.ch/test
docker run docker-registry.ralscha.ch/test
That concludes this tutorial on setting up a self-hosted Docker registry server. Don't forget to back up the registry regularly if you use this setup in production. If you followed the tutorial, all registry data is stored in /var/lib/docker-registry.