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.