Running a Private Docker Image Registry

Registries are the common way to store and distribute Docker images on a network. Operating a private on-premises registry isn't as hard as you probably think.

July 3, 2020 in Docker

Most Docker images are uploaded to a central registry, where they can be downloaded by other users. The largest public registry is Docker Hub. However, anyone is able to operate such a registry on their own machine or network. This article covers the operation and configuration of a basic private registry.

Reasons for a Private Docker Registry

The motivations behind an own image registry are diverse, for sure. One reason may be that you don't want to make every Docker image available to the entire world by uploading it to Docker Hub - especially as there's only one private repository available for free accounts.

Other than that, a private registry may also be interesting for self-hosting folks or organizations who prefer to store their images on-premises rather than in the cloud.

Enterprise Considerations

Generally, the basic image registry used in this article is sufficient for appropriate basic use cases, but it lacks more advanced features. Enterprises have to evaluate whether the plain default registry fits their needs, or if they want to afford a prepackaged registry solution.

In case they go for the latter option, there are a variety of products to choose from. The Docker Trusted Registry shipped with Docker EE probably is the most popular enterprise-grade registry and offers Active Directory access control, automatic vulnerability scans and image signing. Other well-known solutions are Quay by Red Hat or Artifactory by JFrog, for example. All of them offer a more or less similar feature set.

Starting a Local Registry

We are going to start with the most basic registry setup without any configuration. The registry itself is merely a image provided by Docker and should be used in version 2 nowadays. Launching a new registry container is fairly simple:

$ docker container run -d \
  -p 5000:5000 \
  --restart always \
  --name private-registry \
  registry:2

This command starts a new registry container named private-registry, which is available at localhost:5000 on the host system.

You may provide the --rm flag instead of --restart always for our tests, because you'll have to stop and re-create the container multiple times. Also, keep in mind that I'm using $(pwd) in this article, which isn't available on Windows using Docker Desktop. Just use an absolute path like /c/projects/... instead.

Pushing and Pulling Images

There's a simple convention for the docker image push and docker image pull commands: In case the image tag has a prefix in the form <host>:<port>/, Docker interprets this prefix as registry address. That particular address will be used as target for push and as source for pull.

An image can be prefixed with a registry address by re-tagging it. The following commands will download Alpine Linux, create a new tag for the downloaded image and push that tag to our private registry:

$ docker image pull alpine:3.9
$ docker image tag alpine:3.9 localhost:5000/alpine:3.9
$ docker image push localhost:5000/alpine:3.9

As the new image tag has been prefixed with localhost:5000, which is the exact address of the private-registry container, Docker pushes the image to the local registry where it is completely decoupled from the user space Docker storage.

This also means that we can remove the downloaded Alpine image as well as the tagged image:

$ docker image rm alpine:3.9
$ docker image rm localhost:5000/alpine:3.9

Pulling the tagged image from the local registry works analogous to docker image push.

$ docker image pull localhost:5000/alpine:3.9

Alpine Linux is now available again, but this time we've obtained it from our very own image registry.

Outsourcing the Registry Storage

All registry data is stored in /var/lib/registry inside the registry container. This directory is automatically mounted as an anonymous volume. If you want to simplify the management of the registry storage, it is appropriate to create a bind mount or a named volume for this purpose.

Stop the registry container. Then create a new named volume like registry-storage and mount it into the container:

$ docker volume create registry-storage
$ docker container run -d \
  -p 5000:5000 \
  --restart=always \
  --name private-registry \
  --mount type=volume,source=registry-storage,destination=/var/lib/registry \
  registry:2

I will not use this volume in the subsequent commands to keep them as short as possible.

Publishing a Registry on the Network

Our private-registry merely is a local registry up to now, and other users on the network cannot pull images from it. Granting access to other network clients requires a protection via TLS, hence you need to provide a TLS certificate. In the following example, there's a local directory certs that contains a TLS certificate localhost.crt next to a TLS key localhost.key.

After stopping the container again, you may mount the local certs directory into the registry's /certs directory:

$ docker container run -d \
  -p 443:443 \
  --restart=always \
  --name private-registry \
  --mount type=bind,source=$(pwd)/certs,destination=/certs \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

Note that using TLS will cause the container to listen on port 443, so the port mapping and the registry address change at this point. The individual -e options define environment variables for configuring TLS.

Adding Authentication to the Registry

The most basic and simple authentication method for a private registry is htpasswd, which I'm going to demonstrate here. For more serious use cases, you should consider using nginx acting as an authentication proxy.

First of all, I'll create a local directory auth and a file htpasswd stored inside.

$ mkdir auth
$ echo "test-user test-password" >> auth/htpasswd

When starting the registry container, we have the opportunity to mount auth on the host into /auth in the container. Just like with the TLS configuration, any authentication configuration can be specified using the -e option.

Stop the registry container and run the following command:

$ docker container run -d \
  -p 443:443 \
  --restart=always \
  --name private-registry \
  --mount type=bind,source=$(pwd)/certs,destination=/certs \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  \
  --mount type=bind,source=$(pwd)/auth,destination=/auth \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  registry:2

For the sake of clarity, I've separated the authentication flags from the TLS flags with a single \, nevertheless, this command is getting quite long and creating a docker-compose file is really worth the effort. Just consider this heavy command vs. a simple docker-compose up -d.

Let's create a tag prefixed with the new registry address:

$ docker image tag localhost:5000/alpine:3.9 localhost:443/alpine:3.9

Any attempt to push the image is going to fail now, because you're not authenticated yet. You have to login to the registry using docker login localhost:443 and enter your htaccess credentials first.

A More Advanced Configuration

Despite the configuration values for the registry have sane defaults, all of these values should be reviewed when operating a registry in production. If you need to change a configuration value, there are two approaches for doing so:

Overriding some configuration values: The registry configuration is stored in /etc/docker/registry/config.yml. Each property in this YAML file can be overridden by specifying an environment variable using -e. The name of such a variable has the form REGISTRY_PROPERTY, where PROPERTY replicates the YAML path:

storage:
  filesystem:
    rootdirectory: /var/lib/registry

Changing the value of rootdirectory could be accomplished using the following option:

-e REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/my/path

Overriding all configuration values: This pattern can get very tedious and unmanagable when you're overriding too many configuration values. Therefore, it often makes sense to simply mount a custom YAML file as /etc/docker/registry/config.yml, overriding the default YAML file.

$ docker container run -d \
  \ # ...
  --mount type=bind,source=/path/to/custom/config.yml,destination=/etc/docker/registry/config.yml \
  registry:2

Check out the official reference to get an overview for all configuration values available.

Congratulations! The Docker image registry is now up and running, and you're able to upload and distribute images across your network.

💫graph: A library for creating generic graph data structures and modifying, analyzing, and visualizing them.
💫
graph: A library for creating generic graph data structures and modifying, analyzing, and visualizing them.