Running Secured Docker Registry 2.0


Reading Time: 4 minutes

This article was originally published on Container Solutions by Jaroslav Holub, and we are pleased to share it here for Codeship readers.

The new Docker Registry 2.0 was released on April 16th, 2015. It was completely rewritten in Go with added support for the new Docker Registry HTTP API V2 (thus only working with Docker 1.6+), promising to provide faster and more secure distribution of images.

If you work with Docker and for some reason decided not to use the public Docker Hub, a private Docker registry is an essential part of your architecture. But even if you don’t have private images, you will likely need to use your own registry in production/testing for efficiency.

The default installation, however, runs without encryption and authentication. I was wondering what’s involved in securing it. There is an official tutorial on how to configure TLS on a registry server. TLS/SSL is absolutely necessary for any secure setup, but I also wanted to enable an authentication mechanism.

The Configuration Reference document describes two authentication options supported by Docker Registry itself: so-called silly and token solutions. The silly one is apparently only useful for very limited development use cases. The token solution seems to be more serious, but because of the lack of documentation (at the time of writing), I decided to find an alternative approach to secure it.

In this article, I’m going to show you how to set up the Docker Registry 2.0 with username/password authentication and SSL using the official Docker Registry image and a custom-configured nginx as a proxy server.

Note: Docker daemon considers any private registry secure only if it uses transport layer security, a copy of its CA certificate is placed on the Docker host at /etc/docker/certs.d/:/ca.crt, and Docker is able to verify the certificate validity. In other cases (including usage of self-signed certificates), you need to run the Docker daemon with –insecure-registry flag.

First, run the registry:

docker run -v $(pwd)/data:/tmp/registry-dev --name docker-registry registry:2.0

I gave it a name, so we can refer to it from the nginx configuration. Because it’s nice to have stateless containers, I attached a volume where registry stores its data.

Because we want our registry to be accessible only by people who know the password, let’s create a .htpasswd file. You can do it like this: htpasswd -c .htpasswd exampleuser.

The last bits are writing the nginx configuration and finally running the proxy. The nginx config file might look like this:

server { 
  listen 443 ssl; 
  server_name localhost;

  add_header Docker-Distribution-Api-Version: registry/2.0 always;

  ssl on; 
  ssl_certificate /etc/nginx/ssl/docker-registry.crt; 
  ssl_certificate_key /etc/nginx/ssl/docker-registry.key;

  proxy_set_header Host $host; 
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
  proxy_set_header X-Real-IP $remote_addr; 
  proxy_set_header X-Forwarded-Proto $scheme; 
  proxy_set_header X-Original-URI $request_uri; 
  proxy_set_header Docker-Distribution-Api-Version registry/2.0;

  location / { 
    auth_basic "Restricted"; 
    auth_basic_user_file /etc/nginx/.htpasswd; 
    proxy_pass http://docker-registry:5000; 

We need to use the always parameter of the add_header directive, introduced only recently in nginx 1.7.5. The reason is that nginx doesn’t send headers with auth_basic requests by default. Previously, you’d have to use the nginx-extras package that includes the HttpHeadersMore plugin.

Sign up for a free Codeship Account

Looking at the rest of the nginx configuration, it’s SSL only proxy, so we need to attach the certificates to the container to /etc/nginx/ssl/docker-registry.crt and /etc/nginx/ssl/docker-registry.key respectively. There is auth_basic enabled, for which we need to attach /etc/nginx/.htpasswd file, which we just generated.

We use the name of the already-running registry:2.0 container in the proxy_pass directive, together with port 5000, exposed from the container by default.

Now we run the proxy container. You can use the Docker Registry Proxy image, that we created for your convenience. It’s derived from nginx:1.7 and applies the configuration described above. You only need to provide it with REGISTRY_HOST and REGISTRY_PORT variables pointing to the registry container and the SERVER_NAME variable that stands for the nginx server_name directive.

A directory with SSL certificates must be mounted to the container as well as our .htpasswd file. Expose HTTPS port 443 to the host and add a link to our already running docker-registry container:

docker run -p 443:443 \ 
  -e REGISTRY_HOST="docker-registry" \ 
  -e REGISTRY_PORT="5000" \ 
  -e SERVER_NAME="localhost" \ 
  --link docker-registry:docker-registry \ 
  -v $(pwd)/.htpasswd:/etc/nginx/.htpasswd:ro \ 
  -v $(pwd)/certs:/etc/nginx/ssl:ro \ 

Let’s verify that our registry works properly.

docker login -u <username> -p <password> -e <email> localhost:443 
docker pull hello-world 
docker tag hello-world:latest localhost:443/hello-secure-world:latest 
docker push localhost:443/hello-secure-world:latest

You should see a successful push to your private Docker registry, secured with SSL and basic HTTP authentication. Although this setup seems to work, use it at your own risk and please let us know if you spot any issues!

We’re all about making things easier. Have you tried Codeship’s Continuous Integration and Delivery service? Check it out here.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.

We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.