Home | Send Feedback

Self-host Docker registry server on Ubuntu 20.04

Published: 8. May 2020  •  selfhost

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.