Creating Container Image Registry

Posted at Jul 9, 2024

Hi there,

Sometimes you may need to create your own container image registry, either running on your local machine or on cloud/on-prem, where you manage it by yourself. Examples for use cases:

  • The images owned by your organization are high in size and take longer to upload to remote locations,
  • Access control over the images,
  • Image encryption,
  • Requirement to upload images to remote registry during the development process,

What is Container Image Registry?

Container images basically exists in two places; your local machine and remote image registries.

Image registries are distribution points that allow developers to share their container images. When you create an image, the easiest way to share it is to upload it to an image registry.

Nowadays you can find many different image registries, public/private, paid/free. Some of them are as follows:

RegistryPriceType
GitHub PackagesFreeSaaS
Distribution (Formerly known Registry)FreeOn-Prem
Google Artifact RegistryPaidSaaS
Docker HubFree for public imagesSaaS
HarborFreeOn-Prem
Red Hat QuayPaidSaaS
Amazon Elastic Container RegistryPaidSaaS
Azure Container RegistryPaidSaaS

As you can see, all popular cloud providers offer container registry services. If your organization does not need to keep the image registry on on-prem machines, you can use one of these services. In this article, I will try to explain Distribution (formerly known Registry).

Distribution

Distribution, formerly known Registry, is a container image registry software developed by the Docker team and donated to the CNCF foundation. Besides being easy to use, it offers us features such as authentication, hosting images on different storage volumes (S3, Google Cloud Storage, Azure Storage, etc.).

First, make sure you have Docker installed on your system.

Since Distribution is technically a container image, you can also use a different container tool like Podman. I will use Docker in this article.

Then let's use the following command to run our registry:

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

Let's take a closer look at the above command;

  • -d: We make our container run in "detached" mode, basically in the background. In other words, we prevent input and output from connecting to the active terminal.
  • -p: We connect port 5000 in the container to port 5000 on the host machine. If you are already using this port, you can change it by replacing the command with <PORT>:5000.
  • --restart always: We ensure that the container is automatically restarted in case of a voluntary (stop) or involuntary (crash) termination.
  • --name registry: We name our container to differentiate it from the others.
  • registry:2: Although Registry has been renamed to Distribution, it still uses the image named registry for backwards compatibility (I guess). We start our container by choosing this image.

At this stage your image registry is ready for use. Now let's see how to push/pull images into this registry.

Using The Registry

Let's create a simple image as an example:

FROM alpine:3.14
CMD ["sh", "-c", "echo 'Hello, world!'"]

Yes, I guess it couldn't be anything simpler 😃. We wrote a simple inline shell script that puts a message to the screen when executed. To keep the image size low, we used the alpine image as a base.

Save the above file somewhere with the name Dockerfile. Then open a terminal at the saved location and build the image:

$ docker build -t greeter .
[+] Building 2.0s (5/5) FINISHED                                                          docker:default
 => [internal] load build definition from Dockerfile                                                0.0s
 => => transferring dockerfile: 98B                                                                 0.0s
 => [internal] load metadata for docker.io/library/alpine:3.14                                      1.9s
 => [internal] load .dockerignore                                                                   0.0s
 => => transferring context: 2B                                                                     0.0s
 => CACHED [1/1] FROM docker.io/library/alpine:3.14@sha256:0f2d5c38dd7a4f4f733e688e3a6733cb5ab1ac6  0.0s
 => exporting to image                                                                              0.0s
 => => exporting layers                                                                             0.0s
 => => writing image sha256:a1f1a67c51d4954187c2679c0cf661e29b00f2ba6273305e97dfad7ab9fdae42        0.0s
 => => naming to docker.io/library/greeter                                                          0.0s

Yes, our image is now ready to be used on the local docker. We can verify this by testing it with the following command:

$ docker run --rm greeter
Hello, world!

The --rm parameter ensures that the container is automatically deleted when the runtime is complete.

So how do we push this image to our registry? Docker decides where to pull and where to push images based on the host information in the image name. So what does this mean?

By default Docker pulls and pushes all images from/to docker.io. If a location other than docker.io is to be used, it must either be included in the image name or the image must be tagged.

Accordingly, we need to name or tag our greeter image according to our registry running at localhost:5000.

You can think of tagging as creating a symbolic link (ln -s) to an image. Different names pointing to the same place. Apart from this tagging with the docker tag command, there is also tagging to identify different versions of images. This is indicated by writing : after the : character at the end of the image name. For example: hello-world:linux or hello-world:nanoserver-1809

Since we have already created our image, let's apply the tagging method:

$ docker tag greeter localhost:5000/greeter

We have tagged our image, we can also see it with the docker images command:

$ docker images
REPOSITORY               TAG            IMAGE ID       CREATED         SIZE
registry                 2              6a3edb1d5eb6   9 months ago    25.4MB
alpine                   3.14           9e179bacf43c   15 months ago   5.61MB
greeter                  latest         a1f1a67c51d4   15 months ago   5.61MB
localhost:5000/greeter   latest         a1f1a67c51d4   15 months ago   5.61MB

Now we can also point to our image via the new tag we created. Now let's push our image to our registry using the docker push <image name> command:

$ docker push localhost:5000/greeter
Using default tag: latest
The push refers to repository [localhost:5000/greeter]
9733ccc39513: Pushed 
latest: digest: sha256:90bdb97f2d1958c14c8082bf7740fe5e6f9ac9c6e9376706ab1669dae7a16cc0 size: 527

Now let's delete the other images to make sure that our image is actually pushed to the registry we created:

$ docker rmi greeter localhost:5000/greeter
Untagged: greeter:latest
Untagged: localhost:5000/greeter:latest
Untagged: localhost:5000/greeter@sha256:90bdb97f2d1958c14c8082bf7740fe5e6f9ac9c6e9376706ab1669dae7a16cc0
Deleted: sha256:a1f1a67c51d4954187c2679c0cf661e29b00f2ba6273305e97dfad7ab9fdae42

We also remove the image named localhost:5000/greeter that we just tagged. If we want to delete the image completely, we need to remove the other tags pointing to it.

Yes we deleted the other images, the only copy of our image is in our image registry that we run locally:

$ docker run --rm localhost:5000/greeter
Unable to find image 'localhost:5000/greeter:latest' locally
latest: Pulling from greeter
f7dab3ab2d6e: Already exists 
Digest: sha256:90bdb97f2d1958c14c8082bf7740fe5e6f9ac9c6e9376706ab1669dae7a16cc0
Status: Downloaded newer image for localhost:5000/greeter:latest
Hello, world!

As you can see, docker cannot find our image locally and starts pulling it from our registry. Then it runs it and shows us the output.

In the next article of this series, we will examine the authorization process for our registry inshallah. See you soon 👋