If you are working for an organization and want to keep your Docker images in-house for quick deployment, then hosting a private Docker repository is perfect. Having a private docker registry allows you to own your image distribution pipeline and have tighter control over image storage and distribution. You can integrate your registry with your CI/CD system improving your workflow.

This tutorial will teach you how to set up and use a private Docker registry on a Ubuntu 22.04 server using Amazon S3 as a storage location.

Prerequisites

  • Two Linux servers with Ubuntu 22.04. One server will act as the registry host, while the other one will be used as a client to send requests and receive images from the host.

  • A registered domain name pointing to the host server. We will be using registry.example.com for our tutorial.

  • A non-root user with sudo privileges on both machines.

  • Make sure everything is updated.

    $ sudo apt update
    $ sudo apt upgrade
    
  • Few packages that your system needs.

    $ sudo apt install wget curl nano software-properties-common dirmngr apt-transport-https gnupg2 ca-certificates lsb-release ubuntu-keyring unzip -y
    

    Some of these packages may already be installed on your system.

Step 1 – Configure Firewall

The first step is to configure the firewall. Ubuntu comes with ufw (Uncomplicated Firewall) by default.

Check if the firewall is running.

$ sudo ufw status

You should get the following output.

Status: inactive

Allow SSH port so the firewall doesn’t break the current connection on enabling it.

$ sudo ufw allow OpenSSH

Allow HTTP and HTTPS ports as well.

$ sudo ufw allow http
$ sudo ufw allow https

Enable the Firewall

$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Check the status of the firewall again.

$ sudo ufw status

You should see a similar output.

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443                        ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)

Step 2 – Install Docker and Docker Compose

This step is required on both the server and the client machines.

Ubuntu 22.04 ships with an older version of Docker. To install the latest version, first, import the Docker GPG key.

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Create a Docker repository file.

$ echo 
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu 
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Update the system repository list.

$ sudo apt update

Install the latest version of Docker.

$ sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Verify that it is running.

$ sudo systemctl status docker
? docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-04-13 09:37:09 UTC; 3min 47s ago
TriggeredBy: ? docker.socket
       Docs: https://docs.docker.com
   Main PID: 2106 (dockerd)
      Tasks: 7
     Memory: 26.0M
        CPU: 267ms
     CGroup: /system.slice/docker.service
             ??2106 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

By default, Docker requires root privileges. If you want to avoid using sudo every time you run the docker command, add your username to the docker group.

$ sudo usermod -aG docker $(whoami)

You will need to log out of the server and back in as the same user to enable this change or use the following command.

$ su - ${USER}

Confirm that your user is added to the Docker group.

$ groups
navjot wheel docker

Step 3 – Configure Docker Registry

Create user directories

Create a directory for the registry configuration.

$ mkdir ~/docker-registry

Switch to the docker-registry directory.

$ cd ~/docker-registry

Create a directory to store the HTTP authentication password, Nginx configuration files, and SSL certificates.

$ mkdir auth

Create another directory to store Nginx logs.

$ mkdir logs

Create Amazon S3 Bucket

You can store the registry data and the images on your server or use a cloud hosting service. For our tutorial, we will be using the Amazon S3 cloud service.

The next step is to set up the configuration file with a few important settings. These settings can also be defined in the docker-compose.yml file, but having a separate file is much better.

Create a bucket with the following settings.

  • ACL should be disabled.
  • Public access to the bucket should be disabled.
  • Bucket versioning should be disabled.
  • Enable Bucket encryption using Amazon S3 managed keys. (SSE-S3)
  • Object lock should be disabled.

Create an IAM user with the following policy.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": "arn:aws:s3:::S3_BUCKET_NAME"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:ListMultipartUploadParts",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::S3_BUCKET_NAME/*"
    }
  ]
}

Replace the S3_BUCKET_NAME with the name of your S3 bucket.

Note down the secret key, secret value, and the bucket region of your bucket to be used later.

Create Docker Compose File

Create the docker-compose.yml file and open it for editing.

$ nano docker-compose.yml

Paste the following code in it.

services:
  registry:
    image: registry:2
    restart: always
    environment:
      - REGISTRY_STORAGE=s3
      - REGISTRY_STORAGE_S3_REGION=us-west-2
      - REGISTRY_STORAGE_S3_BUCKET=hf-docker-registry
      - REGISTRY_STORAGE_S3_ENCRYPT=true
      - REGISTRY_STORAGE_S3_CHUNKSIZE=5242880
      - REGISTRY_STORAGE_S3_SECURE=true
      - REGISTRY_STORAGE_S3_ACCESSKEY=AKIA3FIG4NVFNXKQXMSJ
      - REGISTRY_STORAGE_S3_SECRETKEY=FBRIrALgLzBqepWUydA7uw9K lljakKdJU8qweeG
      - REGISTRY_STORAGE_S3_V4AUTH=true
      - REGISTRY_STORAGE_S3_ROOTDIRECTORY=/image-registry
      - REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory
      - REGISTRY_HEALTH_STORAGEDRIVER_ENABLED=false
  nginx:
    image: "nginx:alpine"
    ports:
      - 443:443
    links:
      - registry:registry
    volumes:
      - ./auth:/etc/nginx/conf.d
      - ./auth/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./logs:/var/log/nginx
      - /etc/letsencrypt:/etc/letsencrypt

Save the file by pressing Ctrl X and entering Y when prompted.

Let us go through what we have set up in our compose file.

  1. The first step is to grab the latest image of version 2 of the Docker registry from the hub. We are not using the latest tag because it may cause problems in the case of a major version upgrade. Setting it to 2 allows you to grab all 2.x updates while preventing being auto-upgraded to the next major version, which can introduce breaking changes.

  2. The registry container is set to restart always in the case of a failure or an unexpected shutdown.

  3. We have set various environment variables for Amazon S3 storage. Let us go through them quickly.

    • REGISTRY_STORAGE sets the type of storage. We have selected s3 since we are using Amazon S3.
    • REGISTRY_STORAGE_S3_REGION sets the region of your S3 bucket.
    • REGISTRY_STORAGE_S3_BUCKET sets the name of your S3 bucket.
    • REGISTRY_STORAGE_S3_ENCRYPT – set it to true if you have enabled Bucket encryption.
    • REGISTRY_STORAGE_S3_CHUNKSIZE sets the size of upload chunks. It should be larger than 5MB (5 * 1024 * 1024).
    • REGISTRY_STORAGE_S3_SECURE – set it to true if you are going to use HTTPS.
    • REGISTRY_STORAGE_S3_ACCESSKEY and REGISTRY_STORAGE_S3_SECRETKEY – User credentials you grabbed after creating your IAM user.
    • REGISTRY_STORAGE_S3_V4AUTH – set it to true if you use v4 of AWS authentication. If you are getting errors relating to S3 login, set it to false.
    • REGISTRY_STORAGE_S3_ROOTDIRECTORY – sets the root directory in your bucket under which your registry data will be stored.
    • REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR – sets the location for the Cache. In our case, we are storing it in memory. You can also set it to use Redis.
    • REGISTRY_HEALTH_STORAGEDRIVER_ENABLED – Set it to false to disable the Registry’s storage health check service. There is a bug with the Registry that can cause issues if you don’t set it to false.
  4. Docker registry communicates via port 5000, which is what we have exposed in our server to the docker.

  5. ./auth:/etc/nginx/conf.d mapping ensures that all Nginx’s settings are available in the container.

  6. ./auth/nginx.conf:/etc/nginx/nginx.conf:ro maps the Nginx settings file from the system to one in the container in read-only mode.

  7. ./logs:/var/log/nginx allows access to the Nginx’s logs on the system by mapping to the Nginx logs directory in the container.

  8. Docker registry’s settings are stored in the /etc/docker/registry/config.yml file in the container, and we have mapped it to the config.yml file in the current directory, which we will create in the next step.

Set up Authentication

To set up the HTTP authentication, you need to install the httpd-tools package.

$ sudo apt install apache2-utils -y

Create the password file in the ~/docker-registry/auth directory.

$ htpasswd -Bc ~/docker-registry/auth/nginx.htpasswd user1
New password:
Re-type new password:
Adding password for user user1

The -c flag instructs the command to create a new file, and the -B flag is to use the bcrypt algorithm supported by Docker. Replace user1 with a username of your choice.

If you want to add more users, run the command again, but without the -c flag.

$ htpasswd -B ~/docker-registry/auth/registry.password user2

Now, the file will be mapped to the registry container for authentication.

Step 4 – Install SSL

We need to install Certbot to generate the SSL certificate. You can either install Certbot using Ubuntu’s repository or grab the latest version using the Snapd tool. We will be using the Snapd version.

Ubuntu 22.04 comes with Snapd installed by default. Run the following commands to ensure that your version of Snapd is up to date.

$ sudo snap install core && sudo snap refresh core

Install Certbot.

$ sudo snap install --classic certbot

Use the following command to ensure that the Certbot command can be run by creating a symbolic link to the /usr/bin directory.

$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

Run the following command to generate an SSL Certificate.

$ sudo certbot certonly --standalone --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m [email protected] -d registry.example.com

The above command will download a certificate to the /etc/letsencrypt/live/registry.example.com directory on your server.

Generate a Diffie-Hellman group certificate.

$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096

Check the Certbot renewal scheduler service.

$ sudo systemctl list-timers

You will find snap.certbot.renew.service as one of the services scheduled to run.

NEXT                        LEFT          LAST                        PASSED        UNIT                      ACTIVATES
.....
Sun 2023-04-14 00:00:00 UTC 19min left    Sat 2023-02-25 18:04:05 UTC n/a          snap.certbot.renew.timer  snap.certbot.renew.service
Sun 2023-04-14 00:00:20 UTC 19min left    Sat 2023-02-25 10:49:23 UTC 14h ago      apt-daily-upgrade.timer   apt-daily-upgrade.service
Sun 2023-04-14 00:44:06 UTC 3h 22min left Sat 2023-02-25 20:58:06 UTC 7h ago       apt-daily.timer           apt-daily.service

Do a dry run of the process to check whether the SSL renewal is working fine.

$ sudo certbot renew --dry-run

If you see no errors, you are all set. Your certificate will renew automatically.

Copy the Dhparam file to the container

Copy the Diffie-Hellman group certificate to the ~/docker-registry/auth directory, which will be mapped to the container.

$ sudo cp /etc/ssl/certs/dhparam.pem ~/docker-registry/auth

Step 5 – Configure Nginx

The next step involves configuring the Nginx server as a front-end proxy for the Docker registry server. The Docker registry comes with an in-built server operating at port 5000. We will put it behind Nginx.

Create and open the file ~/docker-registry/auth/nginx.conf for editing.

$ sudo nano ~/docker-registry/auth/nginx.conf

Paste the following code in it.

events {
    worker_connections  1024;
}

http {

  upstream docker-registry {
    server registry:5000;
  }

  ## Set a variable to help us decide if we need to add the
  ## 'Docker-Distribution-Api-Version' header.
  ## The registry always sets this header.
  ## In the case of nginx performing auth, the header is unset
  ## since nginx is auth-ing before proxying.
  map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
    '' 'registry/2.0';
  }

  server {
    listen 443 ssl http2;
    server_name registry.example.com;

    # SSL
    ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/registry.example.com/chain.pem;

    access_log  /var/log/nginx/registry.access.log;
    error_log   /var/log/nginx/registry.error.log;

    # Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;
    ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
    ssl_session_cache shared:SSL:10m;
    ssl_dhparam /etc/nginx/conf.d/dhparam.pem;
    resolver 8.8.8.8;

    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;

    # required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
    chunked_transfer_encoding on;

    location /v2/ {
      # Do not allow connections from docker 1.5 and earlier
      # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
      if ($http_user_agent ~ "^(docker/1.(3|4|5(?!.[0-9]-dev))|Go ).*$" ) {
        return 404;
      }

      # To add basic authentication to v2 use auth_basic setting.
      auth_basic "Registry realm";
      auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd;

      ## If $docker_distribution_api_version is empty, the header is not added.
      ## See the map directive above where this variable is defined.
      add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

      proxy_pass                          http://docker-registry;
      proxy_set_header  Host              $http_host;   # required for docker client's sake
      proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
      proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header  X-Forwarded-Proto $scheme;
      proxy_read_timeout                  900;
    }
  }
}

Save the file by pressing Ctrl X and entering Y when prompted once finished.

Step 6 – Launch Docker Registry

Switch to the Docker Registry’s directory.

$ cd ~/docker-registry

Launch the docker container.

$ docker compose up -d

Check the status of the containers.

$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED              STATUS              PORTS                                      NAMES
3328b7e36bb2   nginx:alpine   "https://www.howtoforge.com/docker-entrypoint.…"   About a minute ago   Up 3 seconds        80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp   docker-registry-nginx-1
bf7cdfc0e013   registry:2     "https://www.howtoforge.com/entrypoint.sh /etc…"   About a minute ago   Up About a minute   5000/tcp                                 docker-registry-registry-1

Log in to the Docker registry.

$ docker login -u=user1 -p=password https://registry.example.com

You will get the following output.

WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /home/username/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

You can also open the URL https://registry.example.com/v2/ in your browser, and it will ask for a username and password. You should see an empty page with {} on it.

You can check the URL on the terminal using curl.

$ curl -u user1 -X GET https://registry.example.com/v2/
Enter host password for user 'user1':
{}

Download the latest Ubuntu docker image.

$ docker pull ubuntu:latest

Tag this image for the private registry.

$ docker tag ubuntu:latest registry.example.com/ubuntu2204

Push the image to the registry.

$ docker push registry.example.com/ubuntu2204

Test whether the push has been successful.

$ curl -u user1 -X GET https://registry.example.com/v2/_catalog
Enter host password for user 'user1':
{"repositories":["ubuntu2204"]}

Enter your Nginx authentication password when prompted, and you will see the list of repositories available via the registry.

Log out using the terminal to clear the credentials.

$ docker logout https://registry.example.com
Removing login credentials for registry.example.com

Check the list of Docker images currently available for use.

$ docker images
REPOSITORY                            TAG       IMAGE ID       CREATED       SIZE
registry                             2         8db46f9d7550   2 weeks ago   24.2MB
nginx                                alpine    8e75cbc5b25c   2 weeks ago   41MB
ubuntu                               latest    08d22c0ceb15   5 weeks ago   77.8MB
registry.example.com/ubuntu2204      latest    08d22c0ceb15   5 weeks ago   77.8MB

Step 7 – Access and Use the Docker registry from the Client Machine

Login to your client-server. In step 1, we installed Docker on the client machine.

Login to the private Docker registry from the client machine.

$ docker login -u=user1 -p=password https://registry.example.com

Pull the Ubuntu image from the registry.

$ docker pull registry.example.com/ubuntu2204

List all the images on your client machine.

$ docker images
REPOSITORY                        TAG        IMAGE ID       CREATED         SIZE
registry.example.com/ubuntu2204   latest     08d22c0ceb15   5 weeks ago   77.8MB

Create and launch a container using the downloaded image.

$ docker run -it registry.example.com/ubuntu2204 /bin/bash

You will be logged in to the Shell inside the Ubuntu container.

[email protected]:

Run the following command to check the Linux version.

[email protected]$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Now, you can start using your Docker registry from your client machines.

Conclusion

This concludes our tutorial on setting up a private Docker registry on a Ubuntu 22.04 server that uses Amazon S3 as storage. If you have any questions, post them in the comments below.