In this blog post, I show you how to install Docker and Docker registry server on Ubuntu 20.04.
I run all these steps as the root user. If you are not root, either prepend sudo
to the commands or use the
simulate initial login option of sudo
to start an interactive shell as root: sudo -i
Docker ¶
First, update all installed packages.
apt update
apt full-upgrade
Install Docker, start the daemon and enable it so it will start when the host boots up.
apt install docker.io
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 image
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 Ubuntu 20.04 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 default port 22 to another random port (/etc/ssh/sshd_config
).
In this installation 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 ¶
Docker registry is like a Git repository where you can push images into 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 make sure
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 here the standard settings of the Docker registry server.
UFW / Docker Issue ¶
At this point, we have to fix the UFW / Docker issue. Docker and UFW both create iptables
rules.
When you map a container port with -p
, Docker creates iptables rules and overwrites the rules from UFW.
Ports are closed according to ufw status
but in reality are open to the public.
In our installation, we started the Docker registry server on port 5000, and this port is now open to the public.
You can check that when you run a port scan with nmap
from another computer.
nmap <ip_address>
The output of nmap
shows the open ports.
PORT STATE SERVICE
5000/tcp open upnp
Note that port 35353 does not appear on the list because nmap
only scans by default the most common 1,000 ports.
You can specify the -p-
option to scan all 65,535 ports.
Fortunately, there is a fix for this issue. Add the following rules at the end of
/etc/ufw/after.rules
# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN
-A DOCKER-USER -j ufw-user-forward
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER
Restart UFW
systemctl restart ufw
Run the port scan with nmap
again. Port 5000 should no longer be accessible.
For a more in-depth explanation of what's going on and why this fix solves the problem check out this GitHub repository: https://github.com/chaifeng/ufw-docker
Instead of manually change the configuration file, you can also use the script from this project. See this section of the documentation on how to install and run it.
Reverse proxy ¶
Now we can no longer connect to the Docker registry server from outside, but the purpose of a registry is that we can 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 reverse proxy.
It is worth noting that Docker does not recommend binding your registry to localhost:5000 without authentication. This is what we do here. This creates a potential loophole in your Docker Registry security because anyone who can log on to the server can push images without authentication.
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 such a file. Here with the user demouser
and password mysupersecretpassword
.
docker run --rm --entrypoint htpasswd registry:2 -Bbn demouser mysupersecretpassword > /etc/nginx/nginx.htpasswd
Note this protects only access from the outside to the registry. As mentioned before, somebody logging on to the server can push images without authentication.
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 entered the configuration for HTTP (port 80). The next step is to create a TLS certificate and configure HTTPS (port 443). The certbot command-line tool conveniently performs these two tasks for us.
certbot --nginx
When you run this command for the first time, it asks you about your email,
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 the Quarkus starter application. I run the following commands on my development computer.
mvn io.quarkus:quarkus-maven-plugin:1.13.6.Final: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. Relevant here is that the tag starts with the address of the registry server (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 into a file.
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 about setting up a self-hosted Docker registry server. Don't forget to regularly back up the registry if
you use this setup in an productive environment. If you followed the tutorial, you find all the data of the Docker registry in /var/lib/docker-registry
.