Private Container Registry and Clients

What is a Container Registry?

The containers are created from the images. These images need to be stored and used from a scalable and secure repository. Like the other repositories, the container registry also must provide a fast way to pull and push images with correct policies and credentials.

The most popular container registry hosting the images is Docker Hub. The Docker and Kubernetes are usually using the images via this service. There are many public images freely available, but you don’t have full control over the registry, and day by day, you will meet with some restrictions. Moreover, when it comes to using private images, security is a concern, and it may also be expensive.

Therefore, hosting your private registry can be useful in many situations. Private registries offer many different storage and authentication options and can be customized according to your personal needs.

Why Use a Private Container Registry?

Here are some basic reasons for using your personal private registry instead of a public registry like DockerHub.

  • You have full control over the storage location of the private registry.
  • You can set up policies as you wish
  • Wide options to secure and share your images
  • Special configuration options for logging, authentication, load balancing, etc.

There are some images like DockerHub’s registry and CNCF’s Harbor to configure and use as a private registry. In this study, we will go on with DockerHub’s registry image.

AWS ec2-instance Security Settings for Container Registry

We will deploy the registry to an AWS ec2-instance for the showcase. It will host the image repository. That’s why it is important to emphasize the security group permissions.

HTTP   TCP 80   IP: 0.0.0.0/0 -
HTTP   TCP 443, IP: 0.0.0.0/0 -
SSH    TCP 22   IP: 0.0.0.0/0 -
Custom TCP 5000 IP: 0.0.0.0/0 -
Custom TCP 5000 IP: ::/0 -
1*Fd9GnYUDMoUHvbf5J W3Qw                                                              Security Settings for AWS ec2-instance

Create a Simple Registry

Let’s create a folder to start our journey at the home directory;

mkdir -p docker-hub/data
chmod 777 -R docker-hub/data

The data folder will store the registry data.

We will use docker-compose structure to create and manage the registry.

To create a simple registry, we can use a basic registry image from Docker Hub. The docker-compose.yaml file content is as follows:

version: '3'
services:
  docker-registry:
    image: registry:2
    restart : always
    container_name: docker-registry
    ports:
    - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./data:/data

 The directory structure is as follows:

docker-hub
├── data
└── docker-compose.yaml

Now, run the docker-compose under docker-hub directory.

docker-compose up -d

Pushing an Image to the Registry

The registry is ready to store the images. To do that, we need to create an image that is labeled correctly. The correct convention for labeling is as follows:

<ip>:<port-number>/<image-name>:<tag>
<dns-name>:<port-number>/<image-name>:<tag>

For example, we can pull an image from the docker hub, or create an image from scratch and tag it for our private registry.

docker pull alpine
docker tag alpine localhost:5000/my-alpine

To push the image, we need to login into our private registry if it requires the credentials.

docker login localhost:5000

Now we can push the image using the push command:

docker push localhost:5000/my-alpine

Pulling an Image from the Registry

The pulling of images from the private registry is very straightforward. Again, to pull the image, we need to login into our private registry if it requires credentials.

docker login localhost:5000

Now we can pull the image using the pull command:

docker push localhost:5000/my-alpine

Adding Web Service to the Private Registry

It is a good idea to monitor or see the images via a web browser, as in Docker Hub. To realize this, we can use konradkleine/docker-registry-frontend image. There is enough configuration information on the related documentation page. The docker-compose.yaml file is updated to capture this feature.

version: '3'
services:
  docker-registry:
    image: registry:2
    restart : always
    container_name: docker-registry
    ports:
    - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./data:/data
docker-registry-ui:
    image: konradkleine/docker-registry-frontend:v2
    restart : always
    container_name: docker-registry-ui
    ports:
    - 80:80
    environment:
      ENV_DOCKER_REGISTRY_HOST: ip-172-31-82-125.ec2.internal
      ENV_DOCKER_REGISTRY_PORT: 5000

For the ENV_DOCKER_REGISTRY_HOST, we could use a service name, but as we intend to use this registry as a service, we need a secure SSL connection and a reachable ip or dns name. That is why we used ip-172–31–82–125.ec2.internal, which is reachable from the AWS network internally. A DNS name can also be used instead of this static IP. You can also set the storage location with the help of an environment variable, REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY.

We have added the my-nginx and httpd images to the registry. The web browser screenshots are presented.

1*oWUEBUOpNarYavUj fcGDw
1*EiE1L BMbcLaWvRbJqo85A                                                              Web interface screenshots of private container registry

Adding a Certificate to the Registry

For a secure connection and remote client access, you need certificates. We will show to configure a secure connection with SSL certificates. If you already acquired a .cert and .key from your Certification Authority, then you just need the copy them to under certs directory. Go to docker-hub and then,

mkdir certs
chmod 777 -R certs

Now copy the .crt and .key file to certs directory.

If you don’t have them, here is a way of creating the Certification Authorization Key. For this purpose, we need the openssl package. If openssl not present, install it.

yum -y install openssl

Let’s create certification keys for docker-registry. Command prompt must be at the same level as certs folder. Run the command as follows;

openssl req \
  -newkey rsa:4096 -nodes -sha256 -keyout ./certs/domain.key \
  -x509 -days 3650 -out ./certs/domain.crt

When creating keys, Common Name (eg, your name or your server’s hostname) part is critical. It must be the hostname/ip of the docker-registry host. When AWS considered, private ip/hostname of docker-registry host can be used. The private IP of an ec2 instance is like ip-172–31–82–125.ec2.internal. You can go with defaults (just press enter) or enter the appropriate values for the rest.

[ec2-user@ip-172-31-82-125 docker-hub]$ openssl req \
>   -newkey rsa:4096 -nodes -sha256 -keyout ./certs/domain.key \
>   -x509 -days 3650 -out ./certs/domain.crt
Generating a 4096 bit RSA private key
...............................................................................++
.......................................................................................................................................................++
writing new private key to './certs/domain.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:
State or Province Name (full name) []:
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:ip-172-31-82-125.ec2.internal
Email Address []:

Adding a Certificate to the Registry with the Help of openssl.conf File

You can also use a file to set the openssl configuration settings as an alternative. For this, prepare an openssl.conf file for your needs. We will use the following settings for the parameters:

[ req ]
distinguished_name = req_distinguished_name
x509_extensions     = req_ext
default_md         = sha256
prompt             = no
encrypt_key        = no
[ req_distinguished_name ]
countryName            = "US"
localityName           = "Bristow"
organizationName       = "Clarusway"
organizationalUnitName = "Clarusway"
commonName             = "ip-172-31-82-125.ec2.internal"
emailAddress           = "test@example.com"
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS = "ip-172-31-82-125.ec2.internal"

Run the command as follows;

openssl req \
 -x509 -newkey rsa:4096 -days 3650 -config openssl.conf \
 -keyout certs/domain.key -out certs/domain.crt

The command output should look like this.

openssl req \
>  -x509 -newkey rsa:4096 -days 3650 -config openssl.conf \
>  -keyout certs/domain.key -out certs/domain.crt
Generating a 4096 bit RSA private key
...........................................++
...............++
writing new private key to 'certs/domain.key'
-----

The folder structure should be like this.

docker-hub
├── certs
│   ├── domain.crt
│   └── domain.key
├── data
└── docker-compose.yaml

Now, it is time to update the docker-compose.yaml file to include the certificates.

version: '3'
services:
  docker-registry:
    image: registry:2
    restart : always
    container_name: docker-registry
    ports:
    - "5000:5000"
    environment:
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./certs:/certs
      - ./data:/data
docker-registry-ui:
    image: konradkleine/docker-registry-frontend:v2
    container_name: docker-registry-ui
    ports:
    - 443:443
    environment:
      ENV_DOCKER_REGISTRY_HOST: ip-172-31-82-125.ec2.internal
      ENV_DOCKER_REGISTRY_PORT: 5000
      ENV_USE_SSL: "yes"
      ENV_DOCKER_REGISTRY_USE_SSL: 1
    volumes:
      - ./certs/domain.crt:/etc/apache2/server.crt:ro 
      - ./certs/domain.key:/etc/apache2/server.key:ro

This time, for the web browser link, we have to use https connection. In our case, https://ec2-35-172-118-174.compute-1.amazonaws.com/ will be used. As we get a certificate for a private IP address (ip-172–31–82–125.ec2.internal), we can ignore the security warning and go on.

1*FIhNyfEi0BlPbaoE4ayecg
                                                              Web Interface after SSL Activation

Client Machine Settings to Use the Registry

The domain.crt must be copied to the clients. The path should be /etc/docker/certs.d/<private-docker-hub-ip or dns-name>:<port-no>/. In our case, copy the file to the clients and then at clients move the file to the correct path with root privileges (with sudo or root user):

[root@docker-client1 ~]# mkdir -p /etc/docker/certs.d/ip-172-31-82-125.ec2.internal:5000/
[root@docker-client1 ~]# cp -rf ./domain.crt /etc/docker/certs.d/ip-172-31-82-125.ec2.internal:5000/

The domain.crt can also be copied to /root/domain.crt /etc/docker/certs.d/ip-172–31–82–125.ec2.internal:5000/ directory at registry (private-docker-hub) node.

After this setting, we can pull and push images at the clients with a secure connection.

Creating the Authentification File

In the registry, for authentication, we will use an username and a password. To create the password, we will need htpasswd package to be installed.

If this package is not installed, for ubuntu-based Linux OS;

sudo apt install apache2-utils -y

For fedora-centos-based Linux OS;

sudo yum install httpd-tools -y

After verification of the presence of htpassw package, we can create a folder to go on our journey. Let’s create the folders to hold the password file;

mkdir -p docker-hub/auth

This is the folder to store our password file. Next, the password for a selected user can be created. Let’s create a username and password.

cd docker-hub/auth
htpasswd -Bc registry.password clarusway

The last parameter is the name of the user; in this case clarusway. After executing the command, you will be prompted to enter your password. In this study, clarusway is selected for both username and password.

Final docker-compose files with Certification Authorization Key and username and password authentication;

version: '3'
services:
  docker-registry:
    image: registry:2
    restart : always
    container_name: docker-registry
    ports:
    - "5000:5000"
    environment:
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./certs:/certs
      - ./auth:/auth
      - ./data:/data
docker-registry-ui:
    image: konradkleine/docker-registry-frontend:v2
    restart : always
    container_name: docker-registry-ui
    ports:
    - 443:443
    environment:
      ENV_DOCKER_REGISTRY_HOST: ip-172-31-82-125.ec2.internal
      ENV_DOCKER_REGISTRY_PORT: 5000
      ENV_USE_SSL: "yes"
      ENV_DOCKER_REGISTRY_USE_SSL: 1
    volumes:
      - ./certs/domain.crt:/etc/apache2/server.crt:ro 
      - ./certs/domain.key:/etc/apache2/server.key:ro

The folder structure should be like this;

docker-hub
├── auth
│   └── registry.password
├── certs
│   ├── domain.crt
│   └── domain.key
├── data
└── docker-compose.yaml

Now, run the docker-compose under docker-hub directory. Let’s check the web interface. When the default page loaded, we see the Welcome Screen. After hitting the Browse repositories button, a login attempt is observed.

1*qNtaCZdgwKP jRfTv1F3jQ
1*Y7AIKOhDWD3hRUeUUG5o5w
                                                              Web Interface after Authentication Activation

Sample Usage At Clients

The client should log in and use the reachable IP(DNS)&port number of this registry to pull and push the images.

After authentication setting activation, we have to log in to ip-172–31–82–125.ec2.internal:5000 to pull and push the images at the clients.

docker login ip-172-31-82-125.ec2.internal:5000
docker pull ip-172-31-82-125.ec2.internal:5000/my-alpine
docker tag <image>:tag ip-172-31-82-125.ec2.internal:5000/<new-name>:tag
docker push ip-172-31-82-125.ec2.internal:5000/<new-name>:tag

A sample image pull showcase is as follows:

[ec2-user@ip-172-31-45-163 ~]$ docker login ip-172-31-82-125.ec2.internal:5000
Username: clarusway
Password: 
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[ec2-user@ip-172-31-45-163 ~]$ docker pull ip-172-31-82-125.ec2.internal:5000/my-alpine:latest
latest: Pulling from my-alpine
Digest: sha256:074d3636ebda6dd446d0d00304c4454f468237fdacf08fb0eeac90bdbfa1bac7
Status: Downloaded newer image for ip-172-31-82-125.ec2.internal:5000/my-alpine:latest
ip-172-31-82-125.ec2.internal:5000/my-alpine:latest
[ec2-user@ip-172-31-45-163 ~]$

I hope this study addresses the issues of hosting and use a private container registry. You can freely and securely use the containers in popular tools like Docker, contained, Mesos Containerizer, CoreOS rkt, and LXC Linux Containers. I have explained how to use private registries with Kubernetes in the “How to use images from a private container registry for Kubernetes: AWS ECR, Hosted Private Container Registry”.

References

https://gabrieltanner.org/blog/docker-registry

https://www.learnitguide.net/2018/07/create-your-own-private-docker-registry.html

https://www.youtube.com/watch?v=SEpR35HZ_hQ

https://hub.docker.com/r/konradkleine/docker-registry-frontend

https://github.com/justmeandopensource/docker/tree/master/docker-compose-files/docker-registry