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=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
- 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.
-
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.
-
The registry container is set to restart always in the case of a failure or an unexpected shutdown.
-
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.
-
Docker registry communicates via port 5000, which is what we have exposed in our server to the docker.
-
./auth:/etc/nginx/conf.d
mapping ensures that all Nginx's settings are available in the container. -
./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. -
./logs:/var/log/nginx
allows access to the Nginx's logs on the system by mapping to the Nginx logs directory in the container. -
Docker registry's settings are stored in the
/etc/docker/registry/config.yml
file in the container, and we have mapped it to theconfig.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 name@example.com -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 "/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 "/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.
root@647899f255db:
Run the following command to check the Linux version.
root@a2da49fdbea9$ 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.