Set up Docker Registry with an UI

Bruno Delb
Sep 18, 2020 · 6 min read

In this story, we will install Docker Registry on a VM and the cloud and we will add a Docker container to browser the registries from a web browser. From my side, I created an ECS instance on Alibaba Cloud. But we could do it everywhere. After creating the ECS instance, I got its public IP address (8.208.91.39) and I created a security group to authorize the following ports:

  • 5000 for the Docker Registry,
  • 8086 for the Docker Registry UI.

Set up Docker and Docker Compose

On the VM, if Docker and Docker Compose are not installed, install them:

  • Docker:
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
systemctl start docker
  • Docker Compose:
curl -L "https://github.com/docker/compose/releases/download/1.27.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

I ran these commands as root.

Let’s verify that Docker and Docker Compose are well installed:

docker run hello-world
docker-compose --version

Set up Docker Registry and Docker Registry UI

Let’s install a Docker Registry from a Docker image.

Create a docker_registry directory:

mkdir docker_registry
cd docker_registry

Download the image registry:2:

docker pull registry:2

The output is:

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
docker.io/library/registry:2

Create a directory to store the Docker images in /tmp/docker_registry:

mkdir /tmp/docker_registry

Launch Docker Registry by exposing the ports from5000/tcp to 8888/tcp on the host:

docker run -d -p 5000:5000 --name docker_registry -v /tmp/docker_registry:/var/lib/registry registry:2

Display the logs of the container:

docker logs docker_registry

The output is:

time="2020-09-18T11:16:10.29837169Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable." go.version=go1.11.2 instance.id=30e34f3d-9764-4da3-bd03-eea6a8533b6f service=registry version=v2.7.1
time="2020-09-18T11:16:10.298964778Z" level=info msg="redis not configured" go.version=go1.11.2 instance.id=30e34f3d-9764-4da3-bd03-eea6a8533b6f service=registry version=v2.7.1
time="2020-09-18T11:16:10.311692135Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.11.2 instance.id=30e34f3d-9764-4da3-bd03-eea6a8533b6f service=registry version=v2.7.1
time="2020-09-18T11:16:10.312147339Z" level=info msg="listening on [::]:5000" go.version=go1.11.2 instance.id=30e34f3d-9764-4da3-bd03-eea6a8533b6f service=registry version=v2.7.1
time="2020-09-18T11:16:10.312712833Z" level=info msg="Starting upload purge in 24m0s" go.version=go1.11.2 instance.id=30e34f3d-9764-4da3-bd03-eea6a8533b6f service=registry version=v2.7.1

Let’s launch a Docker image for the user interface:

docker run -d \
-p 8086:80 \
--name=docker_registry_ui \
-e REGISTRY_HOST=0.0.0.0 \
-e REGISTRY_PORT=443 \
-e REGISTRY_PROTOCOL=https \
-e SSL_VERIFY=false \
-e ALLOW_REGISTRY_LOGIN=true \
-e REGISTRY_ALLOW_DELETE=true \
parabuzzle/craneoperator:latest

The container is created:

Unable to find image 'parabuzzle/craneoperator:latest' locally
latest: Pulling from parabuzzle/craneoperator
Image docker.io/parabuzzle/craneoperator:latest uses outdated schema1 manifest format. Please upgrade to a schema2 image for better future compatibility. More information at https://docs.docker.com/registry/spec/deprecated-schema-v1/
2ccb2024ccae: Pull complete
a3ed95caeb02: Pull complete
8a2e3f780f3e: Pull complete
aa8b2e126531: Pull complete
99b73914c700: Pull complete
8f2cc15b97ea: Pull complete
c6e020fdc4a3: Pull complete
c29288212037: Pull complete
Digest: sha256:ea372a0691f577a370eb520e5a1e8abea26a7b8177ecf1b7f96d4932d9ea5e7f
Status: Downloaded newer image for parabuzzle/craneoperator:latest
ceb37593a1e43b2b8eab1768baba49aab389023fe43ae22bac0825841ffbec63

Let’s display the logs of the container:

docker logs docker_registry_ui

The output:

11:17:17 web.1  | started with pid 7
11:17:17 web.1 | I, [2020-09-18T11:17:17.498324 #7] INFO -- : Refreshing Gem list
11:17:17 web.1 | I, [2020-09-18T11:17:17.832954 #7] INFO -- : listening on addr=0.0.0.0:80 fd=9
11:17:17 web.1 | I, [2020-09-18T11:17:17.833463 #7] INFO -- : worker=0 spawning...
11:17:17 web.1 | I, [2020-09-18T11:17:17.835436 #7] INFO -- : worker=1 spawning...
11:17:17 web.1 | I, [2020-09-18T11:17:17.836524 #7] INFO -- : worker=2 spawning...
11:17:17 web.1 | I, [2020-09-18T11:17:17.837351 #10] INFO -- : worker=0 spawned pid=10
11:17:17 web.1 | I, [2020-09-18T11:17:17.837666 #10] INFO -- : worker=0 ready
11:17:17 web.1 | I, [2020-09-18T11:17:17.838662 #7] INFO -- : master process ready
11:17:17 web.1 | I, [2020-09-18T11:17:17.839639 #12] INFO -- : worker=1 spawned pid=12
11:17:17 web.1 | I, [2020-09-18T11:17:17.839953 #12] INFO -- : worker=1 ready
11:17:17 web.1 | I, [2020-09-18T11:17:17.840662 #15] INFO -- : worker=2 spawned pid=15
11:17:17 web.1 | I, [2020-09-18T11:17:17.840991 #15] INFO -- : worker=2 ready

Verify locally that the webserver is running :

curl http://0.0.0.0:8086

We get the output:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crane Operator</title>

So, all is ok. Let’s clean the environment by deleting the two containers:

docker stop docker_registry_ui
docker rm docker_registry_ui
docker stop docker_registry
docker rm docker_registry

Improvement by using Docker Compose

Now, let’s create a Docker Compose file to run all these Docker images and let’s create the credentials (admin /mypassword):

cat << EOF > docker-compose.yml
version: '3'
services:
docker-registry:
image: registry:2
volumes:
- "/tmp/docker_registry:/var/lib/registry"
ports:
- "5000:5000"
restart: always
docker-registry-ui:
image: parabuzzle/craneoperator:latest
ports:
- "8086:80"
environment:
- REGISTRY_HOST=docker-registry
- REGISTRY_PORT=5000
- REGISTRY_PROTOCOL=http
- SSL_VERIFY=false
- USERNAME=admin
- PASSWORD=mypassword
restart: always
depends_on:
- docker-registry
EOF

Launch the stack Docker Registry with an user interface (parabuzzle/craneoperator) :

docker-compose up -d

Verify that the webserver is running :

curl http://0.0.0.0:8086

We get the output:

11:26:23 web.1  | started with pid 7
11:26:23 web.1 | I, [2020-09-18T11:26:23.588554 #7] INFO -- : Refreshing Gem list
11:26:23 web.1 | I, [2020-09-18T11:26:23.960667 #7] INFO -- : listening on addr=0.0.0.0:80 fd=9
11:26:23 web.1 | I, [2020-09-18T11:26:23.961184 #7] INFO -- : worker=0 spawning...
11:26:23 web.1 | I, [2020-09-18T11:26:23.963064 #7] INFO -- : worker=1 spawning...
11:26:23 web.1 | I, [2020-09-18T11:26:23.964394 #7] INFO -- : worker=2 spawning...
11:26:23 web.1 | I, [2020-09-18T11:26:23.965255 #10] INFO -- : worker=0 spawned pid=10
11:26:23 web.1 | I, [2020-09-18T11:26:23.966364 #7] INFO -- : master process ready
11:26:23 web.1 | I, [2020-09-18T11:26:23.966738 #10] INFO -- : worker=0 ready
11:26:23 web.1 | I, [2020-09-18T11:26:23.967560 #12] INFO -- : worker=1 spawned pid=12
11:26:23 web.1 | I, [2020-09-18T11:26:23.967856 #12] INFO -- : worker=1 ready
11:26:23 web.1 | I, [2020-09-18T11:26:23.968627 #15] INFO -- : worker=2 spawned pid=15
11:26:23 web.1 | I, [2020-09-18T11:26:23.968917 #15] INFO -- : worker=2 ready
11:26:39 web.1 | 172.19.0.1 - - [18/Sep/2020:11:26:39 +0000] "GET / HTTP/1.1" 401 - 0.0230
11:26:44 web.1 | 172.19.0.1 - - [18/Sep/2020:11:26:44 +0000] "GET / HTTP/1.1" 401 - 0.0164

We get a 401 HTTP error because we aren’t authenticated. Correct.

Testing Docker Registry

Now, let’s verify all is ok for the Docker Registry. On a desktop, pull the image alpine then push it on theDocker Registry:

docker pull alpine

The output is:

Using default tag: latest
latest: Pulling from library/alpine
df20fa9351a1: Already exists
Digest: sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest

Let’s tag this image:

docker tag alpine 8.208.91.39:5000/alpine

And push it to your Docker Registry:

docker push 8.208.91.39:5000/alpine

We get an error:

The push refers to repository [8.208.91.39:5000/alpine]
Get https://8.208.91.39:5000/v2/: http: server gave HTTP response to HTTPS client

As we configure Docker Registry on HTTP instead of HTTPS, we have to authorize the connection to the Docker client. On a MacOS laptop, let’s open Docker Desktop and in the preferences, in the tab Docker Engine, enter the following configuration:

{ "insecure-registries":["8.208.91.39:5000"] }
Image for post
Image for post

On Linux, we would put this configuration in the /etc/docker/daemon.json file.

Now, let’s try again to push the alpine on the Docker Registry :

docker push 8.208.91.39:5000/alpine

This time, it works because we authorized Docker Engine to use a not HTTPS Docker Registry:

The push refers to repository [8.208.91.39:5000/alpine]
50644c29ef5a: Pushed
latest: digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 size: 528

In a Web browser on the desktop, login on the Docker Registry UI by visiting the webpage http://8.208.91.39:8086/. We have to authenticate with the credentials.

Then the list of the containers in the Docker Registry is displayed:

Image for post
Image for post

We can see the alpine image. Great, all works !

Ok, so now, we know how to set up Docker Registry with a HTML UI. Sure the Docker Registry is not secure by using HTTPS. Generally, the best practice is to use SSL termination with a proxy server like Nginx. If we use a Load Balancer (like SLB on Alibaba Cloud), we can benefit its SSL termination feature.

The Startup

Medium's largest active publication, followed by +756K people. Follow to join our community.

Bruno Delb

Written by

Interests in the full lifecycle: design, Agile Coaching, development, testing, DevOps, Cloud, Management 3.0, ITIL. It defines me.

The Startup

Medium's largest active publication, followed by +756K people. Follow to join our community.

Bruno Delb

Written by

Interests in the full lifecycle: design, Agile Coaching, development, testing, DevOps, Cloud, Management 3.0, ITIL. It defines me.

The Startup

Medium's largest active publication, followed by +756K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store