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 images 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 Rocky Linux 8 based server using Amazon S3 as a storage location.

Prerequisites

  • Two Linux servers with Rocky Linux 8. 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.

Step 1 – Configure Firewall

The first step is to configure the firewall. Rocky Linux uses Firewalld Firewall. Check the firewall’s status.

$ sudo firewall-cmd --state
running

The firewall works with different zones, and the public zone is the default one that we will use. List all the services and ports active on the firewall.

$ sudo firewall-cmd --permanent --list-services

It should show the following output.

cockpit dhcpv6-client ssh

Allow HTTP and HTTPS ports.

$ sudo firewall-cmd --permanent --add-service=http
$ sudo firewall-cmd --permanent --add-service=https

Recheck the status of the firewall.

$ sudo firewall-cmd --permanent --list-services

You should see a similar output.

cockpit dhcpv6-client http https ssh

Reload the firewall to enable the changes.

$ sudo firewall-cmd --reload

Step 2 – Install Docker and Docker Compose

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

Install the official Docker repository.

$ sudo dnf install yum-utils
$ sudo yum-config-manager 
    --add-repo 
    https://download.docker.com/linux/centos/docker-ce.repo

Install Docker.

$ sudo dnf install docker-ce docker-ce-cli containerd.io

Enable and run the Docker daemon.

$ sudo systemctl enable docker --now

Add your system user to the Docker group to avoid using sudo to run Docker commands.

$ sudo usermod -aG docker $(whoami)

Login again to your server after logging out to enable the change.

Download and install the latest stable release of Docker Compose.

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

Apply executable permissions to the downloaded binary file.

$ sudo chmod  x /usr/local/bin/docker-compose

Install the Docker-compose Bash Completion script.

$ sudo curl 
    -L https://raw.githubusercontent.com/docker/compose/1.29.2/contrib/completion/bash/docker-compose 
    -o /etc/bash_completion.d/docker-compose

Reload your profile settings to make the bash-completion work.

$ source ~/.bashrc

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 the 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.Advertisement

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.

version: '3.3'
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=AKIA3FIG4NVFCJ6STMUA
      - REGISTRY_STORAGE_S3_SECRETKEY=j9sA/fw6EE9TVj5KRDhm/7deye aYDPXttkGbdaX
      - 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 which 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 dnf install httpd-tools

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

To install an SSL certificate using Let’s Encrypt, we need to download the Certbot tool, which is available from the Epel repository.

Install EPEL repository and Certbot.

$ sudo dnf install epel-release 
$ sudo dnf install certbot

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 -out /etc/ssl/certs/dhparam.pem 4096

Test the renewal of the certificate.

$ sudo certbot renew --dry-run

If the dry run succeeds, it means your certificates will be automatically renewed.

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. 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.d/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.

Configure SELinux to allow network connections for the Private Docker Registry.

$ sudo setsebool -P httpd_can_network_connect on

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
88d6addc1687   nginx:alpine   "https://www.howtoforge.com/docker-entrypoint.…"   5 minutes ago   Up 5 minutes   80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp   docker-registry_nginx_1
2b112edc1c72   registry:2     "https://www.howtoforge.com/entrypoint.sh /etc…"   5 minutes ago   Up 5 minutes   5000/tcp                                        docker-registry_registry_1

Log in to the Docker registry.

$ docker login -u=testuser -p=testpassword https://registry.example.com

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 testuser -X GET https://registry.nspeaks.xyz/v2/
Enter host password for user 'testuser':
{}

Download the latest Ubuntu docker image.

$ docker pull ubuntu:latest

Tag this image for the private registry.

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

Push the image to the registry.

$ docker push registry.example.com/ubuntu2004

Test whether the push has been successful.

$ curl -u testuser -X GET https://registry.nspeaks.xyz/v2/_catalog
Enter host password for user 'testuser':
{"repositories":["ubuntu2004"]}

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

Check the list of Docker images currently available for use.

$ docker images
REPOSITORY                            TAG       IMAGE ID       CREATED       SIZE
registry                              2         d3241e050fc9   5 days ago    24.2MB
nginx                                 alpine    53722defe627   5 days ago    23.4MB
httpd                                 2         118b6abfbf55   5 days ago    144MB
ubuntu                                latest    ff0fea8310f3   2 weeks ago   72.8MB
registry.nspeaks.xyz/ubuntu2004       latest    ff0fea8310f3   2 weeks ago   72.8MB

Step 7 – Access and Use 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=testuser -p=testpassword https://registry.example.com

Pull the Ubuntu image from the registry.

$ docker pull registry.example.com/ubuntu2004

List all the images on your client machine.

$ docker images
REPOSITORY                        TAG        IMAGE ID       CREATED         SIZE
registry.nspeaks.xyz/ubuntu2004   latest     ff0fea8310f3   2 weeks ago     72.8MB

Create and launch a container using the downloaded image.

$ docker run -it registry.example.com/ubuntu2004 /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
NAME="Ubuntu"
VERSION="20.04.4 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.4 LTS"
VERSION_ID="20.04"
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"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

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 Rocky Linux 8 based server that uses Amazon S3 as storage. If you have any questions, post them in the comments below.