The author selected the Free and Open Source Fund to receive a donation as part of the Write for DOnations program.
Docker Registry is an application that manages the storage and delivery of Docker container images. Registries centralize container images and reduce build times for developers. Docker images guarantee the same runtime environment through virtualization, but building an image can involve a significant time investment. For example, rather than installing dependencies and packages separately to use Docker, developers can download a compressed image from a registry that contains all of the necessary components. Furthermore, developers can automate pushing images to a registry using continuous integration tools, such as TravisCI, to seamlessly update images during production and development.
Docker Hub is a free public registry that can host your custom Docker images, but there are situations where you will not want your image to be publicly available. Images typically contain all the code necessary to run an application, so using a private registry can be preferable when using proprietary software.
In this tutorial, you will set up and secure your own private Docker Registry. You will use Docker Compose to define configurations that run your Docker containers and Nginx to forward server traffic from the internet to the running Docker container. Once you’ve completed this tutorial, you will be able to push a custom Docker image to your private registry and pull the image securely from a remote server.
To complete this tutorial, you will need the following:
sudo
non-root user and a firewall. One server will host your private Docker Registry and the other will be your client server.On the host server, you will need to set up:
your_domain
.Running Docker on the command line is useful when starting out and testing containers, but it can become unwieldy with bigger deployments involving multiple containers running in parallel.
With Docker Compose, you write one .yml
file to set up each container’s configuration and information the containers need to communicate with each other. You can use the docker compose
tool to issue commands to all the components that make up your application and control them as a group.
Docker Registry is itself an application with multiple components, so you will use Docker Compose to manage it. To start an instance of the registry, you’ll set up a docker-compose.yml
file to define it and the location on disk where your registry will be storing its data.
You’ll store the configuration in a directory called docker-registry
on the host server. Create it by running:
- mkdir ~/docker-registry
Navigate to it:
- cd ~/docker-registry
Then, create a subdirectory called data
, where your registry will store its images:
- mkdir data
Create and open a file called docker-compose.yml
by running:
- nano docker-compose.yml
Add the following lines, which define a basic instance of a Docker Registry:
version: '3'
services:
registry:
image: registry:latest
ports:
- "5000:5000"
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
volumes:
- ./data:/data
First, you name the first service registry
, and set its image to the registry
, using the latest version. Then, under ports
, you map port 5000
on the host to the port 5000
of the container, which will allow you to send a request to port 5000
on the server and have the request forwarded to the registry.
In the environment
section, you set the REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
variable to /data
, specifying in which volume it should store its data. Then, in the volumes
section, you map the /data
directory on the host file system to /data
in the container, which acts as a passthrough. The data will actually be stored on the host’s file system.
Save and close the file.
You can now start the configuration by running:
- docker compose up
The registry container and its dependencies will be downloaded and started.
Output[+] Running 2/2
⠿ Network docker-registry_default Created 0.1s
⠿ Container docker-registry-registry-1 Created 0.1s
Attaching to docker-registry-registry-1
docker-registry-registry-1 | time="2022-11-19T14:31:20.40444638Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable." go.version=go1.16.15 instance.id=4fb8d420-eaf8-4a69-b740-bdc94fa52d91 service=registry version="v2.8.1+unknown"
docker-registry-registry-1 | time="2022-11-19T14:31:20.404960549Z" level=info msg="redis not configured" go.version=go1.16.15 instance.id=4fb8d420-eaf8-4a69-b740-bdc94fa52d91 service=registry version="v2.8.1+unknown"
docker-registry-registry-1 | time="2022-11-19T14:31:20.412312462Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.16.15 instance.id=4fb8d420-eaf8-4a69-b740-bdc94fa52d91 service=registry version="v2.8.1+unknown"
docker-registry-registry-1 | time="2022-11-19T14:31:20.412803878Z" level=info msg="Starting upload purge in 52m0s" go.version=go1.16.15 instance.id=4fb8d420-eaf8-4a69-b740-bdc94fa52d91 service=registry version="v2.8.1+unknown"
docker-registry-registry-1 | time="2022-11-19T14:31:20.41296431Z" level=info msg="listening on [::]:5000" go.version=go1.16.15 instance.id=4fb8d420-eaf8-4a69-b740-bdc94fa52d91 service=registry version="v2.8.1+unknown"
...
You’ll address the No HTTP secret provided
warning message later in this tutorial.
The last line of the output indicates it has successfully started, listening on port 5000
.
You can press CTRL+C
to stop its execution.
In this step, you have created a Docker Compose configuration that starts a Docker Registry listening on port 5000
. In the next steps, you’ll expose it at your domain and set up authentication.
As part of the prerequisites, you enabled HTTPS at your domain. To expose your secured Docker Registry there, you’ll need to configure Nginx to forward traffic from your domain to the registry container.
You have already set up the /etc/nginx/sites-available/your_domain
file, containing your server configuration. Open it for editing by running:
- sudo nano /etc/nginx/sites-available/your_domain
Find the existing location
block:
...
location / {
...
}
...
You need to forward traffic to port 5000
, where your registry will be listening for traffic. You also want to append headers to the request forwarded to the registry, which provides additional information from the server about the request itself. Replace the existing contents of the location
block with the following lines:
...
location / {
# 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;
}
proxy_pass http://localhost:5000;
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;
}
...
The if
block checks the user agent of the request and verifies that the version of the Docker client is above 1.5 and that it is not a Go
application that’s trying to access. For more explanation on this, you can find the nginx
header configuration in Docker’s registry Nginx guide.
Save and close the file when you’re done. Apply the changes by restarting Nginx:
- sudo systemctl restart nginx
If you receive an error message, double-check the configuration you’ve added.
To confirm that Nginx is properly forwarding traffic to your registry container on port 5000
, run it:
- docker compose up
Then, in a browser window, navigate to your domain and access the v2
endpoint, like so:
https://your_domain/v2
The browser will load an empty JSON object:
{}
In your terminal, you’ll receive output similar to the following:
Outputdocker-registry-registry-1 | time="2022-11-19T14:32:50.082396361Z" level=info msg="response completed" go.version=go1.16.15 http.request.host=your_domain http.request.id=779fe265-1a7c-4a15-8ae4-eeb5fc35de98 http.request.method=GET http.request.remoteaddr=87.116.166.89 http.request.uri="/v2" http.request.useragent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" http.response.contenttype="text/html; charset=utf-8" http.response.duration="162.546µs" http.response.status=301 http.response.written=39
docker-registry-registry-1 | 172.19.0.1 - - [19/Nov/2022:14:32:50 +0000] "GET /v2 HTTP/1.0" 301 39 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
docker-registry-registry-1 | 172.19.0.1 - - [19/Nov/2022:14:32:50 +0000] "GET /v2/ HTTP/1.0" 200 2 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
docker-registry-registry-1 | time="2022-11-19T14:32:50.132472674Z" level=info msg="response completed" go.version=go1.16.15 http.request.host=your_domain http.request.id=0ffb17f0-c2a0-49d6-94f3-af046cfb96e5 http.request.method=GET http.request.remoteaddr=87.116.166.89 http.request.uri="/v2/" http.request.useragent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" http.response.contenttype="application/json; charset=utf-8" http.response.duration=2.429608ms http.response.status=200 http.response.written=2
docker-registry-registry-1 | 172.19.0.1 - - [19/Nov/2022:14:32:50 +0000] "GET /favicon.ico HTTP/1.0" 404 19 "your_domain/v2/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
From the last line, you know that a GET
request was made to /v2/
, which is the endpoint you sent a request to. The container received the request you made due to port forwarding and returned a response of {}
. The code 200
means that the container handled the request successfully.
Press CTRL+C
to stop its execution.
Now that you have set up port forwarding, you’ll improve the security of your registry.
Nginx allows you to set up HTTP authentication for the sites it manages, which you can use to limit access to your Docker Registry. To achieve this, you’ll create an authentication file with htpasswd
and add username and password combinations to it that will be accepted. That process will enable authentication to your registry.
You can obtain the htpasswd
utility by installing the apache2-utils
package. Do so by running:
- sudo apt install apache2-utils -y
You’ll store the authentication file with credentials under ~/docker-registry/auth
. Create it by running:
- mkdir ~/docker-registry/auth
Navigate to it:
- cd ~/docker-registry/auth
Create the first user, replacing username
with the username you want to use. The -B
flag orders the use of the bcrypt
algorithm, which Docker requires:
- htpasswd -Bc registry.password username
Enter a password when prompted. The combination of credentials will be appended to registry.password
.
Note: To add more users, re-run the previous command without -c
:
- htpasswd -B registry.password username
-c
creates a new file, so removing it will update the existing file.
Now that the list of credentials is made, you’ll edit docker-compose.yml
to order Docker to use the file you created to authenticate users. Open it for editing:
- nano ~/docker-registry/docker-compose.yml
Add the highlighted lines:
version: '3'
services:
registry:
image: registry:latest
ports:
- "5000:5000"
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
volumes:
- ./auth:/auth
- ./data:/data
You’ve added environment variables that specify the use of HTTP authentication and provide the path to the file htpasswd
created. For REGISTRY_AUTH
, you specified htpasswd
as its value, which is the authentication scheme you are using, and set REGISTRY_AUTH_HTPASSWD_PATH
to the path of the authentication file. REGISTRY_AUTH_HTPASSWD_REALM
signifies the name of htpasswd
realm.
You also mounted the ./auth
directory to make the file available inside the registry container. Save and close the file.
You can now verify that your authentication works correctly. First, navigate to the main directory:
- cd ~/docker-registry
Then, run the registry by executing:
- docker compose up
In your browser, refresh the page of your domain. You’ll be asked for a username and password.
After providing a valid combination of credentials, you’ll access the page with the empty JSON object:
{}
You’ve successfully authenticated and gained access to the registry. Exit by pressing CTRL+C
in your terminal.
Your registry is now secured and can be accessed only after authentication. You’ll next configure it to run as a background process while being resilient to reboots by starting automatically.
You can ensure that the registry container starts every time the system boots up, or after it crashes, by instructing Docker Compose to always keep it running.
Open docker-compose.yml
for editing:
- nano docker-compose.yml
Add the following line to the registry
block:
...
registry:
restart: always
...
Setting restart
to always
ensures that the container will survive reboots. When you’re done, save and close the file.
You can now start your registry as a background process by passing in -d
:
- docker compose up -d
With your registry running in the background, you can freely close this SSH session, terminal and the registry won’t be affected.
Because Docker images may be very large in size, you’ll next increase the maximum file size that Nginx will accept for uploads.
Before you can push an image to the registry, you need to ensure that your registry will be able to handle large file uploads. The default size limit of file uploads in Nginx is 1m
, which is not nearly enough for Docker images. To raise it, you’ll modify the main Nginx config file, located at /etc/nginx/nginx.conf
.
Open it for editing:
- sudo nano /etc/nginx/nginx.conf
Add the highlighted line to the http
section:
...
http {
client_max_body_size 16384m;
...
}
...
The client_max_body_size
parameter is now set to 16384m
, making the maximum upload size equal to 16GB.
Save and close the file.
Restart Nginx to apply the configuration changes:
- sudo systemctl restart nginx
In this step, you updated the file size allowed by Nginx. You can now upload large images to your Docker Registry without Nginx blocking the transfer or erroring out.
Now that your Docker Registry server is running and accepting large file sizes, you can try pushing an image to it. Since you don’t have any images readily available, you’ll use the ubuntu
image from Docker Hub, a public Docker Registry, to test this.
In a new terminal session for your client server, run the following command to download the ubuntu
image, run it, and get access to its shell:
- docker run -t -i ubuntu /bin/bash
The -i
and -t
flags give you interactive shell access into the container.
Once you’re in, create a file called SUCCESS
by running:
- touch /SUCCESS
By creating this file, you have customized your container. You’ll later use it to check that you’re using exactly the same container.
Exit the container shell by running:
- exit
Now create a new image from the container you’ve just customized:
- docker commit $(docker ps -lq) test-image
The new image is now available locally and you’ll push it to your container registry. First, you have to log in:
- docker login https://your_domain
When prompted, enter the username and password credentials that you defined in Step 3.
The output will be:
Output...
Login Succeeded
Once logged in, rename the created image:
- docker tag test-image your_domain/test-image
Finally, push the newly tagged image to your registry:
- docker push your_domain/test-image
You’ll receive output similar to the following:
OutputUsing default tag: latest
The push refers to a repository [your_domain/test-image]
1cf9c9034825: Pushed
f4a670ac65b6: Pushed
latest: digest: sha256:95112d0af51e5470d74ead77932954baca3053e04d201ac4639bdf46d5cd515b size: 736
You’ve verified that your registry handles user authentication by logging in and that it allows authenticated users to push images to the registry. You’ll now try pulling the image from your registry.
Now that you’ve pushed an image to your private registry, you’ll try pulling from it.
On the main server, log in with the username and password you set up previously:
- docker login https://your_domain
Try pulling the test-image
by running:
- docker pull your_domain/test-image
Docker will download the image. Run the container with the following command:
- docker run -it your_domain/test-image /bin/bash
It will load the shell for the container.
List the files present by running:
- ls
The list of files will include the SUCCESS
file you created earlier, confirming that this container uses the same image you created:
- SUCCESS bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
Exit the container shell:
- exit
You’ve tested pushing and pulling images and finished setting up a secure registry that you can use to store custom images.
In this tutorial, you set up your own private Docker Registry and published a Docker image to it. As mentioned in the introduction, you can also use TravisCI or a similar CI tool to automate pushing to a private registry directly.
By leveraging Docker containers in your workflow, you can ensure that the image containing the code will result in the same behavior on any machine, whether in production or in development. For more information on writing Docker files, you can visit the official docs on best practices.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This curriculum introduces open-source cloud computing to a general audience along with the skills necessary to deploy applications and websites securely to the cloud.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Rubbish article. How come the author access https without even generating or installing a certificate?
I have “unknown blob” error. Do you have any idea how to solve this? Thanks Petr