The author selected the Apache Software Foundation to receive a donation as part of the Write for DOnations program.
Docker Registry is an application that manages storing and delivering 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 also has a free public registry, Docker Hub, 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 is 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 to run your Docker applications and Nginx to forward server traffic from HTTPS 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.
Before you begin this guide, you’ll need the following:
The Docker command line tool is useful for starting and managing one or two Docker containers, but, for full deployment most applications running inside Docker containers require other components to be running in parallel. For example, a lot of web applications consist of a web server, like Nginx, that serves up the application’s code, an interpreted scripting language such as PHP, and a database server like MySQL.
With Docker Compose, you can write one .yml
file to set up each container’s configuration and the information the containers need to communicate with each other. You can then use the docker-compose
command line tool to issue commands to all the components that make up your application.
Docker Registry is itself an application with multiple components, so you will use Docker Compose to manage your configuration. To start an instance of the registry, you’ll set up a docker-compose.yml
file to define the location where your registry will be storing its data.
On the server you have created to host your private Docker Registry, you can create a docker-registry
directory, move into it, and then create a data
subfolder with the following commands:
- mkdir ~/docker-registry && cd $_
- mkdir data
Use your text editor to create the docker-compose.yml
configuration file:
- nano docker-compose.yml
Add the following content to the file, which describes the basic configuration for a Docker Registry:
version: '3'
services:
registry:
image: registry:2
ports:
- "5000:5000"
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
volumes:
- ./data:/data
The environment
section sets an environment variable in the Docker Registry container with the path /data
. The Docker Registry application checks this environment variable when it starts up, and as a result begins to save its data to the /data
folder.
However, as you have included the volumes: - ./data:/data
line, Docker will start to map the /data
directory in that container to /data
on your registry server. The end result is that the Docker Registry’s data all gets stored in ~/docker-registry/data
on the registry server.
The ports
section, with configuration 5000:5000
, tells Docker to map port 5000
on the server to port 5000
in the running container. This allows you to send a request to port 5000
on the server, and have the request forwarded to the registry application.
You can now start Docker Compose to check the setup:
- docker-compose up
You will see download bars in your output that show Docker downloading the Docker Registry image from Docker’s own registry. Within a minute or two, you’ll see output that looks similar to the following (versions might vary):
Output of docker-compose upStarting docker-registry_registry_1 ... done
Attaching to docker-registry_registry_1
registry_1 | time="2018-11-06T18:43:09Z" 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.7.6 instance.id=c63483ee-7ad5-4205-9e28-3e809c843d42 version=v2.6.2
registry_1 | time="2018-11-06T18:43:09Z" level=info msg="redis not configured" go.version=go1.7.6 instance.id=c63483ee-7ad5-4205-9e28-3e809c843d42 version=v2.6.2
registry_1 | time="2018-11-06T18:43:09Z" level=info msg="Starting upload purge in 20m0s" go.version=go1.7.6 instance.id=c63483ee-7ad5-4205-9e28-3e809c843d42 version=v2.6.2
registry_1 | time="2018-11-06T18:43:09Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.7.6 instance.id=c63483ee-7ad5-4205-9e28-3e809c843d42 version=v2.6.2
registry_1 | time="2018-11-06T18:43:09Z" level=info msg="listening on [::]:5000" go.version=go1.7.6 instance.id=c63483ee-7ad5-4205-9e28-3e809c843d42 version=v2.6.2
You’ll address the No HTTP secret provided
warning message later in this tutorial. The output shows that the container is starting. The last line of the output shows it has successfully started listening on port 5000
.
By default, Docker Compose will remain waiting for your input, so hit CTRL+C
to shut down your Docker Registry container.
You have set up a full Docker Registry listening on port 5000
. At this point the registry won’t start unless you bring it up manually. Also, Docker Registry doesn’t come with any built-in authentication mechanism, so it is currently insecure and completely open to the public. In the following steps, you will address these security concerns.
You already have HTTPS set up on your Docker Registry server with Nginx, which means you can now set up port forwarding from Nginx to port 5000
. Once you complete this step, you can access your registry directly at example.com.
As part of the How to Secure Nginx With Let’s Encrypt prerequisite, you have already set up the /etc/nginx/sites-available/example.com
file containing your server configuration.
Open this file with your text editor:
- sudo nano /etc/nginx/sites-available/example.com
Find the existing location
line. It will look like this:
...
location / {
...
}
...
You need to forward traffic to port 5000
, where your registry will be running. You also want to append headers to the request to the registry, which provide additional information from the server with each request and response. Delete the contents of the location
section, and add the following content into that section:
...
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 $http_user_agent
section verifies that the Docker version of the client is above 1.5
, and ensures that the UserAgent
is not a Go
application. Since you are using version 2.0
of the registry, older clients are not supported. For more information, you can find the nginx
header configuration in Docker’s Registry Nginx guide.
Save and exit the file. Apply the changes by restarting Nginx:
- sudo service nginx restart
You can confirm that Nginx is forwarding traffic to port 5000
by running the registry:
- cd ~/docker-registry
- docker-compose up
In a browser window, open up the following url:
https://example.com/v2
You will see an empty JSON object, or:
{}
In your terminal, you’ll see output similar to the following:
Output of docker-compose upregistry_1 | time="2018-11-07T17:57:42Z" level=info msg="response completed" go.version=go1.7.6 http.request.host=cornellappdev.com http.request.id=a8f5984e-15e3-4946-9c40-d71f8557652f http.request.method=GET http.request.remoteaddr=128.84.125.58 http.request.uri="/v2/" http.request.useragent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7" http.response.contenttype="application/json; charset=utf-8" http.response.duration=2.125995ms http.response.status=200 http.response.written=2 instance.id=3093e5ab-5715-42bc-808e-73f310848860 version=v2.6.2
registry_1 | 172.18.0.1 - - [07/Nov/2018:17:57:42 +0000] "GET /v2/ HTTP/1.0" 200 2 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7"
You can see from the last line that a GET
request was made to /v2/
, which is the endpoint you sent a request to from your browser. The container received the request you made, from the port forwarding, and returned a response of {}
. The code 200
in the last line of the output means that the container handled the request successfully.
Now that you have set up port forwarding, you can move on to improving the security of your registry.
With Nginx proxying requests properly, you can now secure your registry with HTTP authentication to manage who has access to your Docker Registry. To achieve this, you’ll create an authentication file with htpasswd
and add users to it. HTTP authentication is quick to set up and secure over a HTTPS connection, which is what the registry will use.
You can install the htpasswd
package by running the following:
- sudo apt install apache2-utils
Now you’ll create the directory where you’ll store our authentication credentials, and change into that directory. $_
expands to the last argument of the previous command, in this case ~/docker-registry/auth
:
- mkdir ~/docker-registry/auth && cd $_
Next, you will create the first user as follows, replacing username
with the username you want to use. The -B
flag specifies bcrypt
encryption, which is more secure than the default encryption. Enter the password when prompted:
- htpasswd -Bc registry.password username
Note: To add more users, re-run the previous command without the -c option, (the c
is for create):
- htpasswd registry.password username
Next, you’ll edit the docker-compose.yml
file to tell Docker to use the file you created to authenticate users.
- cd ~/docker-registry
- nano docker-compose.yml
You can add environment variables and a volume for the auth/
directory that you created, by editing the docker-compose.yml
file to tell Docker how you want to authenticate users. Add the following highlighted content to the file:
version: '3'
services:
registry:
image: registry:2
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
For REGISTRY_AUTH
, you have specified htpasswd
, which is the authentication scheme you are using, and set REGISTRY_AUTH_HTPASSWD_PATH
to the path of the authentication file. Finally, REGISTRY_AUTH_HTPASSWD_REALM
signifies the name of htpasswd
realm.
You can now verify that your authentication works correctly, by running the registry and checking that it prompts users for a username and password.
- docker-compose up
In a browser window, open https://example.com/v2
.
After entering username
and the corresponding password, you will see {}
once again. You’ve confirmed the basic authentication setup: the registry only returned the result after you entered the correct username and password. You have now secured your registry, and can continue to using the registry.
You want to ensure that your registry will start whenever the system boots up. If there are any unforeseen system crashes, you want to make sure the registry restarts when the server reboots. Open up docker-compose.yml
:
- nano docker-compose.yml
Add the following line of content under registry:
:
...
registry:
restart: always
...
You can start your registry as a background process, which will allow you to exit the ssh
session and persist the process:
- docker-compose up -d
With your registry running in the background, you can now prepare Nginx for file 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. Although Docker splits large image uploads into separate layers, they can sometimes be over 1GB
. By default, Nginx has a limit of 1MB
on file uploads, so you need to edit the configuration file for nginx
and set the max file upload size to 2GB
.
- sudo nano /etc/nginx/nginx.conf
Find the http
section, and add the following line:
...
http {
client_max_body_size 2000M;
...
}
...
Finally, restart Nginx to apply the configuration changes:
- sudo service nginx restart
You can now upload large images to your Docker Registry without Nginx errors.
You are now ready to publish an image to your private Docker Registry, but first you have to create an image. For this tutorial, you will create a simple image based on the ubuntu
image from Docker Hub. Docker Hub is a publicly hosted registry, with many pre-configured images that can be leveraged to quickly Dockerize applications. Using the ubuntu
image, you will test pushing and pulling to your registry.
From your client server, create a small, empty image to push to your new registry, the -i
and -t
flags give you interactive shell access into the container:
- docker run -t -i ubuntu /bin/bash
After it finishes downloading you’ll be inside a Docker prompt, note that your container ID following root@
will vary. Make a quick change to the filesystem by creating a file called SUCCESS
. In the next step, you’ll be able to use this file to determine whether the publishing process is successful:
- touch /SUCCESS
Exit out of the Docker container:
- exit
The following command creates a new image called test-image
based on the image already running plus any changes you have made. In our case, the addition of the /SUCCESS
file is included in the new image.
Commit the change:
- docker commit $(docker ps -lq) test-image
At this point, the image only exists locally. Now you can push it to the new registry you have created. Log in to your Docker Registry:
- docker login https://example.com
Enter the username
and corresponding password from earlier. Next, you will tag the image with the private registry’s location in order to push to it:
- docker tag test-image example.com/test-image
Push the newly tagged image to the registry:
- docker push example.com/test-image
Your output will look similar to the following:
OutputThe push refers to a repository [example.com/test-image]
e3fbbfb44187: Pushed
5f70bf18a086: Pushed
a3b5c80a4eba: Pushed
7f18b442972b: Pushed
3ce512daaf78: Pushed
7aae4540b42d: Pushed
...
You’ve verified that your registry handles user authentication, and allows authenticated users to push images to the registry. Next, you will confirm that you are able to pull images from the registry as well.
Return to your registry server so that you can test pulling the image from your client server. It is also possible to test this from a third server.
Log in with the username and password you set up previously:
- docker login https://example.com
You’re now ready to pull the image. Use your domain name and image name, which you tagged in the previous step:
- docker pull example.com/test-image
Docker will download the image and return you to the prompt. If you run the image on the registry server you’ll see the SUCCESS
file you created earlier is there:
- docker run -it example.com/test-image /bin/bash
List your files inside the bash shell:
- ls
You will see the SUCCESS
file you created for this image:
SUCCESS bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
You’ve finished setting up a secure registry to which users can push and pull custom images.
In this tutorial you set up your own private Docker Registry, and published a Docker image. 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 and registries into 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 read this Docker tutorial explaining the process.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
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!
Hi,
Would you have an idea why when I do
docker login $URL I received a Error 403
Error response from daemon: login attempt to https://$HOST/v2/ failed with status: 403 Forbidden
While if I do :
http -a $USER https://$HOST/v2/_catalog http: password for $USER@$HOST: HTTP/1.1 200 OK Connection: keep-alive Content-Length: 20 Content-Type: application/json; charset=utf-8 Date: Fri, 15 Mar 2019 10:35:03 GMT Docker-Distribution-Api-Version: registry/2.0 Server: nginx/1.15.9 X-Content-Type-Options: nosniff
{ “repositories”: [] }
It works !
Hi,
I push image to private docker registry. It’s shown unknown blob.
Could u help me?
Where you have
version: '3'
in thedocker-compose.yml
, I think you meant that to be beversion: '2'
.You even reference it as version 2 in the browser window
https://example.com/v2
.I couldn’t get
docker-compose up
to run withversion: '3'
.Nice article all the same.
How can we add push restrictions for some users? Like create a separate user for pull and push the docker images.
Thanks for the tutorial. Everything worked fine but there is no information on how I can delete the test-image from my private registry. Would be great if you added that as well. I can’t be alone to aim for digital hygiene :-)
Unfortunately when I am pushing I get:
The logs of the docker registry container only shows a warning:
Any ideas?
When attempting to login to the registry the following error is returned:
Error response from daemon: Get https://<myServer>/v2/: x509: certificate signed by unknown authority.
The server is correctly protected by a wildcard cert for our domain, and going the the location in a web browser works perfectly.
There seems to be a missing step somewhere in the documentation.
Any help is greatly appreciated
An almost completely awesome tutorial … thanks. These tutorials are a bit like the arch wiki.
Minor but annoying error in the documentation.: It is important that new htpasswd users are added with the -B flag. Without this, docker login will fail.
Currently, it reads:
please correct to
Note: To add more users, re-run the previous command without the -c option, (the c is for create):
htpasswd -B registry.password username
Hi, thanks for this nice work.
Juste a little mistake: “client_max_body_size 2000M” - if you want to setup 2G the value must be 2048 ;)
Great tutorial as always ! I experienced an annoying error, so adding my findings here for posterity:
When pushing a repo to my registry, I first login successfully, but pushing results in an “unauthorized: authentication required” error after the image has already been partially uploaded.
Going through the server logs, I find a cryptic “Unknown Blob” error.
I had to change my nginx config to
proxy_set_header X-Forwarded-Proto https;
instead ofproxy_set_header X-Forwarded-Proto $scheme;
.My server was already hosting some other services, so my previous nginx config probably interfered and was to blame.