How To Set Up a Private Docker Registry on Ubuntu 14.04

Published on October 15, 2014
How To Set Up a Private Docker Registry on Ubuntu 14.04
Not using Ubuntu 14.04?Choose a different version or distribution.
Ubuntu 14.04


Docker is a great tool for deploying your servers. Docker even has a public registry called Docker Hub to store Docker images. While Docker lets you upload your Docker creations to their Docker Hub for free, anything you upload is also public. This might not be the best option for your project.

This guide will show you how to set up and secure your own private Docker registry. By the end of this tutorial you will be able to push a custom Docker image to your private registry and pull the image securely from a different host.

This tutorial doesn’t cover containerizing your own application but only how to create the registry where you can store your deployments. If you want to learn how to get started with Docker itself (as opposed to the registry), you may want to read the How To Install and Use Docker: Getting Started tutorial.

This tutorial has been tested with both the registry server and registry client running Ubuntu 14.04, but it may work with other Debian-based distributions. It also covers version 2.0 of the Docker Registry.

Docker Concepts

If you haven’t used Docker before then it’s worth taking a minute to go through a few of Docker’s key concepts. If you’re already using Docker and just want to know how to get started running your own registry, then please skip ahead to the next section.

For a refresher on how to use Docker, take a look at the excellent Docker Cheat Sheet.

Docker at its core is a way to separate an application and the dependencies needed to run it from the operating system itself. To make this possible Docker uses containers and images. A Docker image is basically a template for a filesystem. When you run a Docker image, an instance of this filesystem is made live and runs on your system inside a Docker container. By default this container can’t touch the original image itself or the filesystem of the host where Docker is running. It’s a self-contained environment.

Whatever changes you make in the container are preserved in that container itself and don’t affect the original image. If you decide you want to keep those changes, then you can “commit” a container to a Docker image (via the docker commit command). This means you can then spawn new containers that start with the contents of your old container, without affecting the original container (or image). If you’re familiar with git, then the workflow should seem quite similar: you can create new branches (images in Docker parlance) from any container. Running an image is a bit like doing a git checkout.

To continue the analogy, running a private Docker registry is like running a private Git repository for your Docker images.


To complete this tutorial, you will need the following:

Step 1 — Installing Package for Added Security

To set up security for the Docker Registry it’s best to use Docker Compose. This way we can easily run the Docker Registry in one container and let Nginx handle security and communication with the outside world in another. You should already have it installed from the Prerequisites section.

Since we’ll be using Nginx to handle our security, we’ll also need a place to store the list of username and password combinations that we want to access our registry. We’ll install the apache2-utils package which contains the htpasswd utility that can easily generate password hashes Nginx can understand:

  1. sudo apt-get -y install apache2-utils

Step 2 — Installing and Configuring the Docker Registry

The Docker command line tool works great for starting and managing a Docker container or two, but most apps running inside Docker containers don’t exist in isolation. To fully deploy most apps you need a few components running in parallel. For example, most web applications are made up of a web server that serves up the app’s code, an interpreted scripting language such as PHP or Ruby (with Rails), and a database server like MySQL.

Docker Compose allows you to write one .yml configuration file for the configuration for each container as well as information about how the containers communicate with each other. You then use the docker-compose command line tool to issue commands to all the components that make up an application.

Since the Docker registry itself is an application with multiple components, we’ll use Docker Compose to manage our configuration.

To start a basic registry the only configuration needed is to define the location where your registry will be storing its data. Let’s set up a basic Docker Compose YAML file to bring up a basic instance of the registry.

First create a folder where our files for this tutorial will live and some of the subfolders we’ll need:

  1. mkdir ~/docker-registry && cd $_
  2. mkdir data

Using your favorite text editor, create a docker-compose.yml file:

  1. nano docker-compose.yml

Add the following contents to the file:

  image: registry:2
    - ./data:/data

The interesting bit here is at the end. The environment section sets an environment variable in the Docker registry container with the path /data. The Docker registry app knows to check this environment variable when it starts up and to start saving its data to the /data folder as a result.

Only in this case, the volumes: - ./data:/data bit is telling Docker that the /data directory in that container should actually map out to /data on our host machine. So the end result is that the Docker registry’s data all gets stored in ~/docker-registry/data on our local machine.

Let’s go ahead and start it up to make sure everything is in order:

  1. cd ~/docker-registry
  2. docker-compose up

You’ll see a bunch of download bars come move across your screen (this is Docker downloading the actual Docker registry image from Docker’s own Docker Registry). If everything went well in a minute or two, you should see output that looks like this (versions might vary):

Output of docker-compose up
registry_1 | time="2015-10-18T23:45:58Z" 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." instance.id=44c828de-c27a-401e-bb2e-38b17e6a4b7b version=v2.1.1 registry_1 | time="2015-10-18T23:45:58Z" level=info msg="redis not configured" instance.id=44c828de-c27a-401e-bb2e-38b17e6a4b7b version=v2.1.1 registry_1 | time="2015-10-18T23:45:58Z" level=info msg="using inmemory blob descriptor cache" instance.id=44c828de-c27a-401e-bb2e-38b17e6a4b7b version=v2.1.1 registry_1 | time="2015-10-18T23:45:58Z" level=info msg="listening on [::]:5000" instance.id=44c828de-c27a-401e-bb2e-38b17e6a4b7b version=v2.1.1 registry_1 | time="2015-10-18T23:45:58Z" level=info msg="Starting upload purge in 1m0s" instance.id=44c828de-c27a-401e-bb2e-38b17e6a4b7b version=v2.1.1

Don’t worry about the No HTTP secret provided message. It’s normal.

Great! At this point you’ve already got a full Docker registry up and running and listening on port 5000 (this was set by the ports: bit in the docker-compose.yml file). At this point the registry isn’t that useful yet — it won’t start unless you bring up the registry manually. Also, Docker registry doesn’t come with any built-in authentication mechanism, so it’s insecure and completely open to the public right now.

Docker Compose will by default stay waiting for your input forever, so go ahead and hit CTRL-C to shut down your Docker registry container.

Step 3 — Setting Up an Nginx Container

Let’s get to work on fixing these security issues. The first step is to set up a copy of Nginx inside another Docker container and link it up to our Docker registry container.

Let’s start by creating a directory to store our Nginx configuration:

  1. mkdir ~/docker-registry/nginx

Now, re-open your docker-compose.yml file in the ~/docker-registry directory:

  1. nano docker-compose.yml

Paste the following into the top of the file:

  image: "nginx:1.9"
    - 5043:443
    - registry:registry
    - ./nginx/:/etc/nginx/conf.d:ro

This will create a new Docker container based on the official Nginx image. The interesting bit here is the links section. It automagically set up a “link” from one Docker container to the another. When the Nginx container starts up, it will be able to reach the registry container at the hostname registry no matter what the actual IP address the registry container ends up having. (Behind the scenes Docker is actually inserting an entry into the /etc/hosts file in the nginx container to tell it the IP of the registry container).

The volumes: section is similar to what we did for the registry container. In this case it gives us a way to store the config files we’ll use for Nginx on our host machine instead of inside the Docker container. The :ro at the end just tells Docker that the Nginx container should only have read-only access to the host filesystem.

Your full docker-compose.yml file should now look like this:

  image: "nginx:1.9"
    - 5043:443
    - registry:registry
    - ./nginx/:/etc/nginx/conf.d
  image: registry:2
    - ./data:/data

Running docker-compose up will now start two containers at the same time: one for the Docker registry and one for Nginx.

We need to configure Nginx before this will work though, so let’s create a new Nginx configuration file.

Create a registry.conf file:

  1. nano ~/docker-registry/nginx/registry.conf

Copy the following into the file:

upstream docker-registry {
  server registry:5000;

server {
  listen 443;
  server_name myregistrydomain.com;

  # SSL
  # ssl on;
  # ssl_certificate /etc/nginx/conf.d/domain.crt;
  # ssl_certificate_key /etc/nginx/conf.d/domain.key;

  # 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/docker/docker/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 plus add_header
    # auth_basic "registry.localhost";
    # auth_basic_user_file /etc/nginx/conf.d/registry.password;
    # add_header 'Docker-Distribution-Api-Version' 'registry/2.0' 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 and exit the file.

Now you can install Nginx and start up the two Docker containers all with one command:

  1. docker-compose up

Nginx doesn’t print any output on startup, but if all went well you’re now running a copy of Nginx that is set up to proxy to your registry container. To test it, let’s use curl to make an HTTP request to our Docker registry directly, and then make another request to our Nginx port. If everything is set up correctly the output will be the same in both cases (as of this writing Docker returns an empty json object “{}”) since Nginx will proxy the request through to the Docker registry.

First, make an HTTP request directly to the Docker registry:

  1. curl http://localhost:5000/v2/

As of this writing Docker returns an empty json object, so you should see:


Now send an HTTP request to the Nginx port:

  1. curl http://localhost:5043/v2/

You should see the same output:


If things are working correctly you’ll see some output in your docker-compose terminal that looks like the below as well:

Output of docker-compose
registry_1 | time="2015-08-11T10:24:53.746529894Z" level=debug msg="authorizing request" environment=development http.request.host="localhost:5043" http.request.id=55c3e2a6-4f34-4b0b-bc57-11c814b4f4d3 http.request.method=GET http.request.remoteaddr= http.request.uri="/v2/" http.request.useragent="curl/7.35.0" instance.id=55634dfc-c9e0-4ec9-9872-6f4930c17759 service=registry version=v2.0.1 registry_1 | time="2015-08-11T10:24:53.747650205Z" level=info msg="response completed" environment=development http.request.host="localhost:5043" http.request.id=55c3e2a6-4f34-4b0b-bc57-11c814b4f4d3 http.request.method=GET http.request.remoteaddr= http.request.uri="/v2/" http.request.useragent="curl/7.35.0" http.response.contenttype="application/json; charset=utf-8" http.response.duration=8.143193ms http.response.status=200 http.response.written=2 instance.id=55634dfc-c9e0-4ec9-9872-6f4930c17759 service=registry version=v2.0.1 registry_1 | - - [11/Aug/2015:10:24:53 +0000] "GET /v2/ HTTP/1.0" 200 2 "" "curl/7.35.0" nginx_1 | - - [11/Aug/2015:10:24:53 +0000] "GET /v2/ HTTP/1.1" 200 2 "-" "curl/7.35.0" "-"

If you see lines with the registry_ prefix (the number after the _ may be different on your machine) then all is well, and Nginx has successfully proxied our HTTP request to the Docker registry.

Go ahead and hit CTRL-C again in your docker-compose terminal to shut down your Docker containers.

Step 4 — Setting Up Authentication

Now that Nginx is proxying requests properly let’s set it up with HTTP authentication so that we can control who has access to our Docker registry. To do that we’ll create an authentication file in Apache format (Nginx can read it too) via the htpasswd utility we installed earlier and add users to it.

Create the first user as follows, replacing USERNAME with the username you want to use:

  1. cd ~/docker-registry/nginx
  2. htpasswd -c registry.password USERNAME

Create a new password for this user when prompted.

If you want to add more users in the future, just re-run the above command without the -c option (the c is for create):

  1. htpasswd registry.password USERNAME

At this point we have a registry.password file with our users set up and a Docker registry available. You can take a peek at the file at any point if you want to view your users (and remove users if you want to revoke access).

Next, we need to tell Nginx to use that authentication file.

Open up ~/docker-registry/nginx/registry.conf in your favorite text editor:

  1. nano ~/docker-registry/nginx/registry.conf

Scroll to the middle of the file where you’ll see some lines that look like this:

# To add basic authentication to v2 use auth_basic setting plus add_header
# auth_basic "registry.localhost";
# auth_basic_user_file /etc/nginx/conf.d/registry.password;
# add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always;

Uncomment the two lines that start with auth_basic as well as the line that starts with add_header by removing the # character at the beginning of the lines. It should then look like this:

# To add basic authentication to v2 use auth_basic setting plus add_header
auth_basic "registry.localhost";
auth_basic_user_file /etc/nginx/conf.d/registry.password;
add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always;

We’ve now told Nginx to enable HTTP basic authentication for all requests that get proxied to the Docker registry and told it to use the password file we just created.

Let’s bring our containers back up to see if authentication is working:

  1. cd ~/docker-registry
  2. docker-compose up

Repeat the previous curl test:

  1. curl http://localhost:5043/v2/

You should get a message complaining about being unauthorized:

Output of curl
<html> <head><title>401 Authorization Required</title></head> <body bgcolor="white"> <center><h1>401 Authorization Required</h1></center> <hr><center>nginx/1.9.7</center> </body> </html>

Now try adding the username and password you created earlier to the curl request:

  1. curl http://USERNAME:PASSWORD@localhost:5043/v2/

You should get the same output you were getting before — the empty json object {}. You should also see the same registry_ output in the docker-compose terminal.

Go ahead and use CTRL-C in the docker-compose terminal to shut down the Docker containers.

Step 5 — Setting Up SSL

At this point we have the registry up and running behind Nginx with HTTP basic authentication working. However, the setup is still not very secure since the connections are unencrypted. You might have noticed the commented-out SSL lines in the Nginx config file we made earlier.

Let’s enable them. First, open the Nginx configuration file for editing:

  1. nano ~/docker-registry/nginx/registry.conf

Use the arrow keys to move around and look for these lines:

server {
  listen 443;
  server_name myregistrydomain.com;

  # SSL
  # ssl on;
  # ssl_certificate /etc/nginx/conf.d/domain.crt;
  # ssl_certificate_key /etc/nginx/conf.d/domain.key;

Uncomment the lines below the SSL comment by removing the # characters in front of them. If you have a domain name set up for your server, change the value of server_name to your domain name while you’re at it. When you’re done, the top of the file should look like this:

server {
  listen 443;
  server_name myregistrydomain.com;

  # SSL
  ssl on;
  ssl_certificate /etc/nginx/conf.d/domain.crt;
  ssl_certificate_key /etc/nginx/conf.d/domain.key;

Save the file. Nginx is now configured to use SSL and will look for the SSL certificate and key files at /etc/nginx/conf.d/domain.crt and /etc/nginx/conf.d/domain.key respectively. Due to the mappings we set up earlier in our docker-compose.yml file the /etc/nginx/conf.d/ path in the Nginx container corresponds to the folder ~/docker-registry/nginx/ on our host machine, so we’ll put our certificate files there.

If you already have an SSL certificate set up or are planning to buy one, then you can just copy the certificate and key files to the paths listed in registry.conf (ssl_certificate and ssl_certificate_key).

You could also get a free signed SSL certificate.

Otherwise we’ll have to use a self-signed SSL certificate.

Signing Your Own Certificate

Since Docker currently doesn’t allow you to use self-signed SSL certificates this is a bit more complicated than usual — we’ll also have to set up our system to act as our own certificate signing authority.

To begin, let’s change to our ~/docker-registry/nginx folder and get ready to create the certificates:

  1. cd ~/docker-registry/nginx

Generate a new root key:

  1. openssl genrsa -out devdockerCA.key 2048

Generate a root certificate (enter whatever you’d like at the prompts):

  1. openssl req -x509 -new -nodes -key devdockerCA.key -days 10000 -out devdockerCA.crt

Then generate a key for your server (this is the file referenced by ssl_certificate_key in our Nginx configuration):

  1. openssl genrsa -out domain.key 2048

Now we have to make a certificate signing request.

After you type this command, OpenSSL will prompt you to answer a few questions. Write whatever you’d like for the first few, but when OpenSSL prompts you to enter the “Common Name” make sure to type in the domain or IP of your server.

  1. openssl req -new -key domain.key -out dev-docker-registry.com.csr

For example, if your Docker registry is going to be running on the domain www.ilovedocker.com, then your input should look like this:

Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:www.ilovedocker.com
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Do not enter a challenge password.

Next, we need to sign the certificate request:

  1. openssl x509 -req -in dev-docker-registry.com.csr -CA devdockerCA.crt -CAkey devdockerCA.key -CAcreateserial -out domain.crt -days 10000

Since the certificates we just generated aren’t verified by any known certificate authority (e.g., VeriSign), we need to tell any clients that are going to be using this Docker registry that this is a legitimate certificate. Let’s do this locally on the host machine so that we can use Docker from the Docker registry server itself:

  1. sudo mkdir /usr/local/share/ca-certificates/docker-dev-cert
  2. sudo cp devdockerCA.crt /usr/local/share/ca-certificates/docker-dev-cert
  3. sudo update-ca-certificates

Restart the Docker daemon so that it picks up the changes to our certificate store:

  1. sudo service docker restart

Warning: You’ll have to repeat this step for every machine that connects to this Docker registry! Instructions for how to do this for Ubuntu 14.04 clients are listed in Step 9 — Accessing Your Docker Registry from a Client Machine.

Step 6 — Testing SSL

Bring up our Docker containers via the now familiar docker-compose up:

  1. cd ~/docker-registry
  2. docker-compose up

Do another curl test from another terminal (only this time using https) to verify that our SSL setup is working properly. Keep in mind that for SSL to work correctly you will have to use the same domain name you typed into the Common Name field earlier while you were creating your SSL certificate.

  1. curl https://USERNAME:PASSWORD@[YOUR-DOMAIN]:5043/v2/

Note: If you are using a self-signed certificate, you will see the following error from curl:

curl: (60) SSL certificate problem: self signed certificate

Use the -k option to tell curl not to verify with the peer:

  1. curl -k https://USERNAME:PASSWORD@[YOUR-DOMAIN]:5043/v2/

For example, if the user and password you set up were sammy and test, and your SSL certificate is for www.example.com, then you would type the following:

  1. curl https://sammy:test@www.example.com:5043/v2/

If all went well curl will print an empty json object {}, and your docker-compose terminal will print the usual registry_ output.

If not, recheck the SSL steps and your Nginx configuration file to make sure everything is correct.

At this point we have a functional Docker registry 2.0 up and running behind an Nginx server which is providing authentication and encryption via SSL. If your firewall is configured to allow access to port 5043 from the outside, then you should be able to login to this Docker registry from any machine docker login https://<YOURDOMAIN> and entering the username and password you set in the earlier section.

Step 7 — Setting SSL Port to 443

Just a couple more steps to do before we’re done: change the port to use the standard SSL port of 443 (optional) and set docker-compose up to start this set of containers on startup.

Let’s start by setting up our dockerized Nginx container to listen on port 443 (the standard port for SSL) rather than the non-standard port 5043 we’ve been using so far. Ports below 1024 are “privileged” ports on Linux though, which means we’re going to have to run our docker-compose container as root.

First open up docker-compose.yml in a text editor:

  1. nano ~/docker-registry/docker-compose.yml

Under the Nginx section you’ll see a ports: section, change the - 5043:443 line (this maps port 5043 on our host machine to port 443 inside the Nginx container) to - 443:443 so that our Nginx container’s port 443 gets mapped to our host machine’s port 443. When finished your docker-compose.yml should look like this:

  image: "nginx:1.9"
    - 443:443
    - registry:registry
    - ./nginx/:/etc/nginx/conf.d:ro
  image: registry:2
    - ./data:/data

Kill your docker-compose session via CTRL-C if it’s still running, and restart it on port 443:

  1. sudo docker-compose up

Note: Only root users can listen to ports below 1024. Notice that you need to use sudo this time with the docker-compose command so Nginx can run on the default SSL port 443.

You should see docker-compose start up as usual.

Let’s try another curl test using our domain name, only this time we won’t specify the :5043 in the URL:


If all went well you should see the usual registry_ output in your docker-compose terminal. You may also want to try running this same curl command from another machine to make sure that your port 443 is being exposed to the outside world.

Go ahead and use CTRL-C in the docker-compose terminal to shut down the Docker containers before moving to the next step.

Step 8 — Starting Docker Registry as a Service

If everything is looking good, let’s go ahead and create an Upstart script so that our Docker registry will start whenever the system boots up.

First let’s remove any existing containers, move our Docker registry to a system-wide location and change its permissions to root:

  1. cd ~/docker-registry
  2. docker-compose rm # this removes the existing containers
  3. sudo mv ~/docker-registry /docker-registry
  4. sudo chown -R root: /docker-registry

Then use your favorite text editor to create an Upstart script:

  1. sudo nano /etc/init/docker-registry.conf

Add the following contents to create the Upstart script (getting Upstart to properly monitor Docker containers is a bit tricky, check out this blog post if you’d like more info about what this Upstart script is doing):

description "Docker Registry"

start on runlevel [2345]
stop on runlevel [016]

respawn limit 10 5

chdir /docker-registry

exec /usr/local/bin/docker-compose up

For more about Upstart scripts, please read this tutorial.

Let’s test our new Upstart script by running:

  1. sudo service docker-registry start

You should see something like this:

docker-registry start/running, process 25303

You can verify that the server is running by executing:

  1. docker ps

The output should look similar to the following (note that the names all start with dockerregistry_

CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                          NAMES
d4b6fef0b4d1        nginx:1.9           "nginx -g 'daemon of   2 minutes ago       Up 2 minutes        80/tcp,>443/tcp   dockerregistry_nginx_1
77668352bd39        registry:2          "registry cmd/regist   2 minutes ago       Up 2 minutes>5000/tcp       dockerregistry_registry_1

Upstart will log the output of the docker-compose command to /var/log/upstart/docker-registry.log. For our final test let’s “live-watch” the log file with tail (the sudo is necessary because upstart logs are written as the root user):

  1. sudo tail -f /var/log/upstart/docker-registry.log

You should see the usual registry_ output. From another terminal or machine go ahead and run our now familiar curl test:


If everything is working correctly curl will print a {} to your terminal and you should see the usual:

registry_1 | time="2015-08-12T08:01:12.241887501Z" level=debug msg="authorizing request" environment=development http.request.host=docker.meatflavoredbeer.com http.request.id=e8d69e16-9448-4c48-afd8-57b1f1302742 http.request.method=GET http.request.remoteaddr= http.request.uri="/v2/" http.request.useragent="curl/7.37.1" instance.id=14d4727b-fda1-463f-8d0e-181f4c70cb17 service=registry version=v2.0.1
registry_1 | time="2015-08-12T08:01:12.242206499Z" level=info msg="response completed" environment=development http.request.host=docker.meatflavoredbeer.com http.request.id=e8d69e16-9448-4c48-afd8-57b1f1302742 http.request.method=GET http.request.remoteaddr= http.request.uri="/v2/" http.request.useragent="curl/7.37.1" http.response.contenttype="application/json; charset=utf-8" http.response.duration=3.359883ms http.response.status=200 http.response.written=2 instance.id=14d4727b-fda1-463f-8d0e-181f4c70cb17 service=registry version=v2.0.1
registry_1 | - - [12/Aug/2015:08:01:12 +0000] "GET /v2/ HTTP/1.0" 200 2 "" "curl/7.37.1"
nginx_1    | - nik [12/Aug/2015:08:01:12 +0000] "GET /v2/ HTTP/1.1" 200 2 "-" "curl/7.37.1" "-"

Step 9 — Accessing Your Docker Registry from a Client Machine

To access your Docker registry from another machine, first add the SSL certificate you created earlier to the new client machine. The file you want is located at ~/docker-registry/nginx/devdockerCA.crt.

You can copy it to the new machine directly or use the below instructions to copy and paste it:

On the registry server, view the certificate:

  1. sudo cat /docker-registry/nginx/devdockerCA.crt

You’ll get output that looks something like this:

Output of sudo cat /docker-registry/nginx/devdockerCA.crt
-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJANiXy7fHSPrmMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTQwOTIxMDYwODE2WhcNNDIwMjA2MDYwODE2WjBF MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAuK4kNFaY3k/0RdKRK1XLj9+IrpR7WW5lrNaFB0OIiItHV9FjyuSWK2mj ObR1IWJNrVSqWvfZ/CLGay6Lp9DJvBbpT68dhuS5xbVw3bs3ghB24TntDYhHMAc8 GWor/ZQTzjccHUd1SJxt5mGXalNHUharkLd8mv4fAb7Mh/7AFP32W4X+scPE2bVH OJ1qH8ACo7pSVl1Ohcri6sMp01GoELyykpXu5azhuCnfXLRyuOvQb7llV5WyKhq+ SjcE3c2C+hCCC5g6IzRcMEg336Ktn5su+kK6c0hoD0PR/W0PtwgH4XlNdpVFqMST vthEG+Hv6xVGGH+nTszN7F9ugVMxewIDAQABo1AwTjAdBgNVHQ4EFgQULek+WVyK dJk3JIHoI4iVi0FPtdwwHwYDVR0jBBgwFoAULek+WVyKdJk3JIHoI4iVi0FPtdww DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkignESZcgr4dBmVZqDwh YsrKeWSkj+5p9eW5hCHJ5Eg2X8oGTgItuLaLfyFWPS3MYWWMzggxgKMOQM+9o3+k oH5sUmraNzI3TmAtkqd/8isXzBUV661BbSV0obAgF/ul5v3Tl5uBbCXObC+NUikM O0C3fDmmeK799AM/hP5CTDehNaFXABGoVRMSlGYe8hZqap/Jm6AaKThV4g6n4F7M u5wYtI9YDMsxeVW6OP9ZfvpGZW/n/88MSFjMlBjFfFsorfRd6P5WADhdfA6CBECG LP83r7/MhqO06EOpsv4n2CJ3yoyqIr1L1+6C7Erl2em/jfOb/24y63dj/ATytt2H 6g== -----END CERTIFICATE-----

Copy that output to your clipboard, and connect to your client machine.

On the client machine, create the certificate directory:

  1. sudo mkdir /usr/local/share/ca-certificates/docker-dev-cert

Open the certificate file for editing:

  1. sudo nano /usr/local/share/ca-certificates/docker-dev-cert/devdockerCA.crt

Paste the certificate contents.

Verify that the file saved to the client machine correctly by viewing the file:

  1. cat /usr/local/share/ca-certificates/docker-dev-cert/devdockerCA.crt

If everything worked properly you’ll see the same text from earlier:

Output of cat /usr/local/share/ca-certificates/docker-dev-cert/devdockerCA.crt
-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJANiXy7fHSPrmMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV ... ... LP83r7/MhqO06EOpsv4n2CJ3yoyqIr1L1+6C7Erl2em/jfOb/24y63dj/ATytt2H 6g== -----END CERTIFICATE-----

Now update the certificates:

  1. sudo update-ca-certificates

You should get output that looks like the following (note the1 added):

Output of sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d....done.

If you don’t have Docker installed on the client yet, do so now (see the Prerequisites section).

Restart Docker to make sure it reloads the system’s CA certificates.

  1. sudo service docker restart

You should now be able to log in to your Docker registry from the client machine:

  1. docker login https://YOUR-DOMAIN

Note that you’re using https://. Enter the username and password you set up earlier (enter whatever you’d like for email if prompted).

Output of docker login
Username: USERNAME Password: PASSWORD Email: Account created. Please see the documentation of the registry http://localhost:5000/v1/ for instructions how to activate it.

You should see the following message:

Output of docker login
Login Succeeded

At this point your Docker registry is up and running! Let’s make a test image to push to the registry.

Step 10 — Publish to Your Private Docker Registry

You are now ready to publish an image to your private Docker registry, but first we have to create an image. We will create a simple image based on the ubuntu image from Docker Hub.

From your client machine, create a small empty image to push to our new registry.

  1. docker run -t -i ubuntu /bin/bash

After it finishes downloading you’ll be inside a Docker prompt. Let’s make a quick change to the filesystem by creating a file called SUCCESS:

  1. touch /SUCCESS

Exit out of the Docker container:

  1. exit

Commit the change:

  1. docker commit $(docker ps -lq) test-image

This 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.

This image only exists locally right now, so let’s push it to the new registry we’ve created.

In the previous step, you logged into your private Docker registry. In case you aren’t still logged in, let’s log in again (note that you want to use https://):

  1. docker login https://YOUR-DOMAIN

Enter the username and password you set up earlier:

Username: USERNAME
Password: PASSWORD
Account created. Please see the documentation of the registry http://localhost:5000/v1/ for instructions how to activate it.

Docker has an unusual mechanism for specifying which registry to push to. You have to tag an image with the private registry’s location in order to push to it. Let’s tag our image to our private registry:

  1. docker tag test-image [YOUR-DOMAIN]/test-image

Note that you are using the local name of the image first, then the tag you want to add to it. The tag does not use https://, just the domain, port, and image name.

Now we can push that image to our registry. This time we’re using the tag name only:

  1. docker push [YOUR-DOMAIN]/test-image

This will take a moment to upload to the registry server. You should see output that ends with something similar to the following:

Output of docker push
latest: digest: sha256:5ea1cfb425544011a3198757f9c6b283fa209a928caabe56063f85f3402363b4 size: 8008

Step 11 — Pull from Your Docker Registry

To make sure everything worked, let’s go back to our original server (where you installed the Docker registry) and pull the image we just pushed from the client. You could also test this from a third server.

If Docker is not installed on your test pull server, go back and follow the installation instructions (and if it’s a third server, the SSL instructions) from Step 6.

Log in with the username and password you set up previously.

  1. docker login https://[YOUR-DOMAIN]

And now pull the image. You want just the “tag” image name, which includes the domain name, port, and image name (but not https://):

  1. docker pull [YOUR-DOMAIN]/test-image

Docker will do some downloading and return you to the prompt. If you run the image on the new machine you’ll see that the SUCCESS file we created earlier is there:

  1. docker run -t -i [YOUR-DOMAIN]/test-image /bin/bash

List your files inside the bash shell:

  1. ls

You should see the SUCCESS file we created earlier for this image:

SUCCESS  bin  boot  dev  etc  home  lib  lib64  media   mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var


Congratulations! You’ve just used your own private Docker registry to push and pull your first Docker container!

Happy Dockering!

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us

About the authors

Default avatar
Tammy Fox


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?

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!

Any chance this article can be updated for setting up Docker Registry 2.0? It’s a Golang implementation now instead of Python - https://github.com/docker/distribution/

First Awesome tutorial!

For those with SSL issues, i highly suggest you check out LetsEncrypt. I was able to add it and use my domain name, worked without issue, no private cert issues! If your domain is setup in DO, just add an A record to your domain ( IE registry … so you have registry.mydomain.io ) and point it to your registry server. Then use the name in the Lets Encrypt SSL generation. Also make sure to use the domain in the nginx config.

I ran the first part of the instructions to install lets encrypt on the getting started page:

$ git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ ./letsencrypt-auto --help

Then generated my cert:

/opt/letsencrypt/letsencrypt-auto certonly --standalone -d registry.mydomain.io

Run a dir on the new cert location so you can see the contents(it also tells you where to find these after they are generated):

dir  /etc/letsencrypt/live/registry.mydomain.io/
cert.pem  chain.pem  fullchain.pem  privkey.pem

Change directory to the guide directory (/docker-registry/nginx), then copy the files to the folder( i also renamed them to match the config in the guide above in this command):

cp /etc/letsencrypt/live/registry.mydomain.io/fullchain.pem domain.crt
cp /etc/letsencrypt/live/registry.mydomain.io/privkey.pem domain.key

Thats it!

To renew the cert ( cause it expires in a few months ):

/opt/letsencrypt/letsencrypt-auto renew -nvv --standalone -d registry.mydomain.io

Then remember to copy the new files to the /docker-registry/nginx folder

Would love to see this guide add a web interface to the registry!

On centos6.5. curl localhost:5000. It could work well. But curl username:password@localhost:8080 . I can not got right message.

it will print

<html> <head><title>502 Bad Gateway</title></head> <body bgcolor=“white”> <center><h1>502 Bad Gateway</h1></center> <hr><center>nginx/1.6.2</center> </body> </html>

Anyone can help me

Is it faster to pull from the registry if your on the same server as the registry?


Thanks, this is brilliant.

It might be worth noting in steps 1-3 can be simplified by using the containerized version of registry rather than installing it locally. e.g. docker run -p 5000:5000 -e GUNICORN_OPTS=[--preload] registry works for me in place of those steps (Note that the -e option seems to be necessary due to a bug in the current (0.9.0) version as documented on the Hub, https://registry.hub.docker.com/_/registry/).

Makes me think I should also be able to run the nginx in a container as well, but haven’t been able to get that working. (I suppose the certificate dance must be done locally in any event?)

For some reason this is not working with the latest release of docker-registry (0.9.0). After I execute gunicorn --access-logfile - --debug -k gevent -b -w 1 docker_registry.wsgi:application I don’t get any arrero message though I can confirm that this was working with version (0.8.1).

Is there any way to install an older version of the registry? Or a way to solve this?

how to create docker user only for pull using nginx from docker registry

how to view available docker images in docker registry from client machine after successfully login

how to check images in docker private registry

In the setting up authentication - when create a user name using ngnix. can i create a user only for pull and not able to push in docker registry? If it is possible then what is the step?

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel