Photo by Dayne Topkin on Unsplash

Photo by Dayne Topkin on Unsplash

Creating a private Docker registry

All Yours

Article from ADMIN 60/2020
A private Docker registry gives you more control over images and protects your Docker Hub credentials.

During testing, a local Docker registry for your container images can be useful. Images stored locally within a mini-registry give you more control over them, with improvements in speed when pulling the images. Also, you might be working with an application that needs access to a registry for one reason or another, but you do not want to give the application your Docker Hub credentials.

In this article, I look at installing a private Docker registry on a local machine (Ubuntu LTS 18.04 with Linux Mint on top) for testing purposes.

On Your Marks

The documentation from Docker on private registries is really detailed, but every now and again a local setup has unusual issues. To begin, you need to make sure that the latest version of Docker Community Edition (Docker CE) is installed. I used the documentation online [1], which you can reference if you get stuck and need more help.

As the root user, run:

$ apt remove docker docker-engine containerd runc

This command removes all other package manager versions or manually installed versions of Docker, so you can ensure that you are definitely using the Docker CE version. The documentation offers some reassurance that any container-related files residing within the /var/lib/docker directory are not deleted during this process.

Next, install the package manager tools you will need (some or all of these packages might already be present), then add Docker's key (success is denoted by a simple OK ), and check the key's fingerprint:

$ apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
$ curl -fsSL | sudo apt-key add -
$ apt-key fingerprint 0EBFCD88

Have a look at the second line of the final command's output (Listing 1) and make sure it is exactly the same as the trusted key.

Listing 1

Checking the Fingerprint

pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid           [ unknown] Docker Release (CE deb) <>
sub   rsa4096 2017-02-22 [S]

Assuming you have AMD64 or x86_64 hardware (see the documentation for ARM options), run:

$ add-apt-repository "deb [arch=amd64]$(lsb_release -cs) stable"

This command adds the Docker repository to your Apt package manager resources in the /etc/apt/sources.list.d/additional-repositories.list file and does not necessarily produce output on success. Because I am using Linux Mint, that command won't quite do what I need, so instead of running that command, I create a file called /etc/apt/sources.list.d/docker.list:

deb [arch=amd64] bionic stable

Finally, run the commands to install the latest Docker CE version:

$ apt update
$ apt install docker-ce docker-ce-cli

The output tells you that two packages will be installed: docker-ce and docker-ce-cli .

By updating your package manager sources, you now have the ability to update your version of Docker CE automatically, in exactly the same way you would other system packages, whenever a newer version becomes available.

Check that Docker Engine is installed and has started up as hoped with the docker ps command.

The Sounds of Science

You can now proceed to install a private Docker registry. The process is relatively simple, but as with anything related to containers, it can be nuanced. The setup involves creating simple htaccess credentials to provide a degree of control over which users can access the image registry. Also, you will create your own self-signed certificates so that the connection to the registry is encrypted and you will know for sure that you are connecting to a known resource.

You might be pleasantly surprised to discover that the installation of the registry component is uber-simple, and you might not be entirely surprised to discover that the clever people at Docker have created a container image to do so. Pull the image for the registry with the command in Listing 2, ensuring that you only use version 2 and not the earlier version with the :1 tag.

Listing 2

Pulling the Registry Image

$ docker pull registry:2
2: Pulling from library/registry
cbdbe7a5bc2a: Pull complete
47112e65547d: Pull complete
46bcb632e506: Pull complete
c1cc712bcecd: Pull complete
3db6272dcbfa: Pull complete
Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d
Status: Downloaded newer image for registry:2

The most basic command to start up the registry (do not run it just yet, because the command will be improved significantly),

$ docker run -d -p 5000:5000 --restart=always --name registry registry:2

shows that (just like Docker Hub) registries run off TCP port 5000, and you are exposing it on your underlying host from the container port with the same number. You are also assuming that if the container fails or the machine reboots, the container will always restart. Finally, you are giving it a name (i.e., registry in this case) that you can reference in scripts and use from other containers.

You could pay for certificates if you like and set up DNS to point at your registry, but I will use self-signed certificates. To begin, create a local directory on which to drop the certs:

$ mkdir certs
$ openssl req -newkey rsa:4096 -nodes -sha256 -key out certs/registry.key-x509 -days 999 -out certs/registry.crt

During the process, choose either your local IP address or DNS name to enter in the Common Name option, and you can mostly ignore the other options by just hitting the Enter key. If you're unsure, you can use another terminal to get your IP address with the command:

$ ip a

In my case, it was After running this openssl command, you should have a registry.crt certificate file and a registry.key private key file (saved in the certs/ subdirectory), which you can use in your docker run command. If for some reason you did not install OpenSSL, simply install the package on Debian derivatives with:

$ apt install openssl

Now you need to create basic authentication with htaccess. The process is also super-simple. (I use the htpasswd binary from the apache2 toolset to create the credentials files.) Although you can save the .htaccess file anywhere, for ease, I save it in the certs/ directory starting with this command:

$ apt install apache2-utils

The output informs you that the following new packages will be installed: apache2-utils , libapr1 , and libaprutil1 . The command

$ htpasswd -Bbn chrisbinnie [password] > certs/htpasswd

creates a simple .htpasswd file; you probably recognize this file from enabling basic authentication for the Apache2 web server and Nginx. If you look inside the file, you will see a username with the password encrypted by the strong BCrypt password hashing:

$ cat certs/htpasswd

Now you are all set to create a private registry.


Log in to the registry just as you would with any other image container registry, such as Docker Hub. Your credentials are those added to your .htpasswd file. The cautious among you would delete the line showing your password from your Bash history.

The more complex docker run command is shown in Listing 3. After running the command, check to see whether the registry is accessible. If it does not start properly, try running the

Listing 3

Complex docker run

$ docker run -d -p 5000:5000 --restart=always --name registry -v "$(pwd)"/certs:/auth -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Log into the registry" -e REGISTRY_AUTH_HTPASSWD_PATH=/certs/htpasswd -v "$(pwd)"/certs:/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt -e REGISTRY_HTTP_TLS_KEY=/certs/registry.key registry:2
docker logs registry

command. More often than not, the paths for the auth and certs are incorrect. Next, run the familiar command:

$ docker ps

In the STATUS column, the output shows the container has been running for a few seconds without restarting. Two important concepts to bear in mind are that, with this particular setup, your locally stored images are available to your Docker Engine as usual, and you also have an image repository that can be accessed via the registry container. At this point, you can log in to your new registry with the credentials you just set up, replacing the IP address with your own (Listing 4).

Listing 4

Logging In

$ docker login <IP_address>:5000
Username: chrisbinnie
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
Login Succeeded

Now that you have logged in to your registry, you should save a container image to the registry to test it. Be warned that you really need to understand the naming convention of container images at this point, so if you are new to containers, copy these commands exactly (altering them slightly to match your IP address or DNS name).

The command in Listing 5 lets you see which images are currently downloaded to your local Docker Engine. To prove that the images in your registry are different from those stored locally, download an image from Docker Hub, rename it (with the tag command), and upload it (with a push command) to your new registry.

Listing 5

See Current Images

$ docker images
REPOSITORY       TAG            IMAGE ID          CREATED            SIZE
registry         2              2d4f4b5309b1      2 months ago       26.2MB

It's important to use the container image's full name. For example, even from Docker Hub, an image's real name isn't (e.g., in the case of the Redis container) just redis:latest , but more like:


On Docker Hub, the full name of popular images is abstracted away from the user for convenience, but for every other registry, it is really important to use the full name.

In this example, the full name of an image (again, using redis:latest as an example) would look like:

To prove that your registry works as hoped, start by pulling redis from Docker Hub:

$ docker pull redis

That pull takes a bit longer than the local registry image pull because it is a larger image. Next, check that it is present locally (Listing 6). You can see that redis is present and is now stored locally. Now rename the image with a tag command,

Listing 6

Check redis Pull

$ docker images
REPOSITORY        TAG            IMAGE ID          CREATED             SIZE
redis             latest         84c5f6e03bf0      45 hours ago        104MB
registry          2              2d4f4b5309b1      2 months ago        26.2MB
$ docker tag redis:latest

and check locally again for two images with the same hash ID (Listing 7).

Listing 7

Check for Renamed Image

$ docker images
REPOSITORY                TAG       IMAGE ID         CREATED           SIZE   latest    84c5f6e03bf0     45 hours ago      104MB
redis                     latest    84c5f6e03bf0     45 hours ago      104MB
registry                  2         2d4f4b5309b1     2 months ago      26.2MB

Next, push the image with the longer name to the new registry (Listing 8). The output shows that you have written to your registry, as hoped.

Listing 8

Push Renamed Image

$ docker push
The push refers to repository []
2e9c060aef92: Pushed
ea96cbf71ac4: Pushed
47d8fadc6714: Pushed
7fb1fa4d4022: Pushed
45b5e221b672: Pushed
07cab4339852: Pushed
latest: digest: sha256:02d2467210e76794c98ae14c642b88ee047911c7e2ab4aa444b0bfe019a41892 size: 1572

For a final test, delete the images stored locally (i.e., the redis images) with the commands,

$ docker rmi redis:latest
$ docker rmi

and check that they are no longer present locally (Listing 9). You can see that no redis images are visible to Docker Engine locally. Now you can try and pull directly from your registry with the command in Listing 10. The results look promising, so check to see what is visible to Docker Engine (Listing 11).

Listing 9

Confirm Removed Images

$ docker images
REPOSITORY          TAG     IMAGE ID            CREATED             SIZE
registry            2       2d4f4b5309b1        2 months ago        26.2MB

Listing 10

Pull from Local Registry

$ docker pull
latest: Pulling from redis
d121f8d1c412: Pull complete
2f9874741855: Pull complete
d92da09ebfd4: Pull complete
bdfa64b72752: Pull complete
e748e6f663b9: Pull complete
eb1c8b66e2a1: Pull complete
Digest: sha256:02d2467210e76794c98ae14c642b88ee047911c7e2ab4aa444b0bfe019a41892
Status: Downloaded newer image for

Listing 11

Current Registry State

$ docker images
REPOSITORY                TAG    IMAGE ID          CREATED             SIZE   latest 84c5f6e03bf0      45 hours ago        104MB
registry                  2      2d4f4b5309b1      2 months ago        26.2MB

You have proven that images are abstracted away from your local storage that Docker Engine normally uses and do indeed exist within the registry's storage.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

comments powered by Disqus