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.
Posted on July 3, 2020
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.
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.
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.
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
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
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
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
container, Docker pushes the image to the local registry where it is completely decoupled from the user space Docker
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.
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.
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
After stopping the container again, you may mount the local
certs directory into the registry's
$ 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.
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
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
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
docker login localhost:443 and enter your htaccess credentials first.
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
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
PROPERTY replicates the YAML path:
storage: filesystem: rootdirectory: /var/lib/registry
Changing the value of
rootdirectory could be accomplished using the following option:
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.
Bind Mounts are a simple and efficient way to share data between a container and its host. They're particularly useful for developers working with Docker containers.