A private Docker registry allows you to share your custom base images within your organization, keeping a consistent, private, and centralized source of truth for the building blocks of your architecture. A private Docker registry gives you better performances for big clusters and high-frequency roll-outs, plus added features like access authentication.

In an earlier post, we had a look at how one could store Docker images in Exoscale’s S3-compatible object storage.

We described how to configure a Docker registry storing images on Exoscale’s Object Storage, yet this keeps the local Docker instance responsible for the processing itself.

To improve availability, a registry would be better hosted on an external server.
Let’s see how to setup a private registry, and then later how to secure the whole thing. We will split the process in two, setting up first and securing the registry later.

How to setup your own private Docker registry

What is a private Docker registry anyway?

After building a Docker image on your machine, it’s possible to run it on the spot. But if you’re a software provider, what if you want to share the image with the whole world? Or, what if you want to privately share the image with your team?

A registry can be considered private if pulling requires authentication

A Docker registry is a place where you can store your images i.e. docker push, and let third-parties get them i.e. docker pull. Docker Hub is the default registry. For example, let’s run:

$ docker run hello-world

In a very simplified way, the process goes like this:

  1. Check if the hello-world image is found locally
  2. If it isn’t, pull it from Docker Hub
  3. Register it in the local Docker. The image is now available locally
  4. Run it via the local Docker daemon

Note that while you can pull freely, pushing still requires some kind of authentication.
A registry can be considered private if pulling requires authentication too.

For example, GitLab, a popular Continuous Integration platform, provides a Docker registry per project among more traditional “build” capabilities, and it can be configured to be freely accessible or private.

However, GitLab’s registry is a solution that is still a bit rough around the edges.
It’s quite hard to remove images (while it’s possible to untag them though), and more importantly, using the SaaS version of Gitlab’s registry is an all-or-nothing option: there’s no way to customize it e.g. integrate it with one’s identity store for authenticated access.

Let’s start from scratch instead, and publish our private registry on Exoscale’s cloud servers.

How to set up a private Docker registry

Good news, a Docker registry is just a Docker image! So, in order to set up a Docker registry, you first need to… setup Docker itself.

How to securely install the latest Docker release

There are two ways to install Docker:

  • From a package: this requires downloading a specific package and manually installing it e.g. dpkg my-package.deb
  • From a repository e.g. apt-get install my-package.

Installing from a repository makes updates to installed packages applied in an automated way.
The biggest advantage of this approach is that the system will always benefit from the latest security patch. In the light of some recent security scandals related to an outdated library/package, it seems there’s no other way.

Let’s choose option 2 using a fresh Ubuntu instance. To install Docker is just as easy as:

$ sudo apt-get update
$ sudo apt-get install docker.io

However, getting the latest and greatest version requires a bit more effort, as the official Ubuntu repository lags behind the Docker release cycle.

To do so, the official Docker package repository should first be added to the list of available repositories. This requires some preliminary setup, as it is a security-sensitive operation.

  1. As key system components, packages should be signed and verified. The provider will sign a package using a private key, and provide a public key so that a third-party can check it’s genuine. Since the check process is handled by the system, we need to provide it with the Docker GPG public key first.

    $ sudo apt-get update
    $ sudo apt-get install apt-transport-https \
                         ca-certificates \
                         curl \
                         software-properties-common
    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    
  2. If somebody hacked the previous site to set its own key, it could impersonate the Docker organization and sign malicious packages. We need to assert this is the correct key:

    $ sudo apt-key fingerprint 0EBFCD88
    

    0EBFCD88 is part of Docker’s public key. This should output something akin to the following:

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

    Notice that the uid contains the string 0EBF CD88 that we searched for, as well as Docker Release (CE deb) <docker@docker.com>. At this point, we have allowed the installation of packages signed by Docker.

  3. Add the proper repository:

    $ sudo add-apt-repository \
         "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
         $(lsb_release -cs) \
         stable"
    
  4. Install Docker (finally):

    $ sudo apt-get update       # Update the local cache with remote repositories data
    $ sudo apt-get install docker-ce
    

To test the correct installation, execute this command:

$ sudo docker run hello-world

This should yield the following:

Hello from Docker!
This message shows that your installation appears to be working correctly.

# And more stuff...

Great, let’s pat ourselves on the back because at this point, Docker is (finally) ready to use. Remember that we had to process all those steps because the out-of-the-box Ubuntu repository doesn’t contain the latest version of Docker.

How to install the Docker registry on a virtual machine

Now is time for the proper Docker registry installation. Interestingly enough, this might be the easiest part of the whole setup process. As stated above, a Docker registry is just a specific running container, registry.

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

That command:

  • downloads the registry image which is tagged 2. This tag references the latest version of the registry at the time of this writing.
  • exposes port 5000 to the host, under the same port
  • gives the container the name registry instead of assigning it a random name

To make sure that the registry is running, a simple docker ps should display the following (abridged for readability):

CONTAINER ID  IMAGE       COMMAND                 PORTS                   NAMES
50b0ff79e9dc  registry:2  "/entrypoint.sh /etc…"  0.0.0.0:5000->5000/tcp  registry

How to push a custom Docker image to a remote private registry

Now, to test that the registry behaves as attended, let’s push a basic image to our brand-new shiny registry. We will use the hello-world image.

The goal is now to push the local image to the registry available remotely.

If you followed the above procedure, you might have noticed the hello-world image is already available on the remote Docker. Now, you might think if you push, nothing will change because the image is already present remotely, but this is actually wrong.

There’s a clear difference between an image available to Docker, and an image stored in a Docker registry.

  • In the first case, it can be listed and run by the Docker daemon to which it belongs.
  • In the second case, it cannot.

For an image stored in a registry to be run requires it to be first pulled to a Docker instance.

There’s one constraint though, the image’s name needs to be prefixed with the registry’s URL (whether domain-based or IP), port included e.g. 159.100.243.157:5000/hello-world.
This is the responsibility of the docker tag command:

$ docker tag hello-world 159.100.243.157:5000/hello-world # Replace with your IP/domain

The new label should now appear:

$ docker images

159.100.243.157:5000/hello-world  latest  4ab4c602aa5e  3 days ago  1.84kB
hello-world                       latest  4ab4c602aa5e  3 days ago  1.84kB

Although listed with two different labels, the very same hello-world image is referenced, so that the disk space is used only once. You can make sure of that by looking at the image ID (4ab4c602aa5e in our case).

Labels are in fact pretty similar to links created by the ln command. If you remove one of the labels, the image will still be available with the other one. In order to remove the image altogether, it has to be referenced without a label.

Now is time to push the image to the registry, with the docker push command:

$ docker push 159.100.243.157:5000/hello-world

Chances are high to get the following error output:

The push refers to repository [159.100.243.157:5000/hello-world]
Get https://159.100.243.157:5000/v2/: http: server gave HTTP response to HTTPS client

Docker expects a secured channel by default, and that’s naturally a very good thing. But TLS adds another layer of complexity, and possible issues, so let’s skip that for now and we’ll come back on the subject a bit later.

Configuring Docker to accept connections to unsecure registries depends on your OS, but it’s quite straightforward. In all cases you will need to update a daemon.json file.

On Linux the .json file is located /etc/docker/daemon.json and assuming no other setting is present in the file, it should look like this:

{
  "insecure-registries" : ["my_registry_address:5000"]
}

You can create the file if does not exist, and you will need to restart Docker afterwards for the changes to take effect. On macOS you do it using the user interface, and the changes will automatically restart the daemon:

  1. Click on the Docker icon
  2. Select Preferences… in the menu
  3. Select the Daemon tab
  4. Check the checkbox named Experimental features
  5. In the first list box, enter the address (URL or IP) of the unsecure registry e.g. 159.100.243.157:5000

Wait a bit for the Docker daemon to restart, then push again to the registry with the same command-line as above. This time, it should be a success:

The push refers to repository [159.100.243.157:5000/hello-world]
428c97da766c: Pushed 
latest: digest: sha256:1a6fd470b9ce10849be79e99529a88371dff60c60aab424c077007f6979b4812 size: 524

The image should now be safely stored on the Docker registry we set up. In order to make sure of that, we can ask this remote registry what images it contains. Fortunately, the registry also offers a web API to query stored images.

From any machine, type:

$ curl -X GET http://159.100.243.157:5000/v2/_catalog

This should return:

{"repositories":["hello-world"]}

Security considerations

Congratulations, you managed to install your own private Docker registry and push your image to it! Remember that at this point, the registry is not secured, and in more than one way.

We have seen while pushing to the registry that Docker expects a secured channel by default, but we have skipped it to keep things simple. Moreover, anybody can push to, or pull from the registry…

You should never leave it in such a state for a real-world setup! If you do, bad things will happen sooner or later, as Aeroflot can attest.

We will show you how to secure your registry in another blog post, follow us on Twitter to stay tuned!

References