Skip to content

Docker-Registry TLS


This guide for installing docker registry with TLS so HTTPS, with self signed certificate

This guide in general copy the non TLS version with some tweeks.


I will install everything related to Docker-registry into its own namespace called docker-registry. So we create that first.

kubectl create namespace docker-registry


Since we are going to store docker images in our personal registry to be later used with OpenFaaS it would be shame if they disappeared every time pod would reschedule to another node.

We need persistent storage that would follow our pods around and provide it with same data all the time.

If you followed my setup you should have longhorn installed.


A persistentVolumeClaim volume is used to mount a PersistentVolume into a Pod. PersistentVolumeClaims are a way for users to "claim" durable storage (such as a GCE PersistentDisk or an iSCSI volume) without knowing the details of the particular cloud environment.

We will create new folder called docker-registry and new file pvc.yaml in it.

mkdir docker-registry
cd  docker-registry
nano pvc.yaml

In our pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
  name: longhorn-docker-registry-pvc
  namespace: docker-registry
    - ReadWriteOnce
  storageClassName: longhorn
      storage: 10Gi

and another one where we will store certificates.

nano pvc_cert.yaml
apiVersion: v1
kind: PersistentVolumeClaim
  name: longhorn-docker-registry-pvc-cert
  namespace: docker-registry
    - ReadWriteOnce
  storageClassName: longhorn
      storage: 10Mi

We are telling Kubernetes to use Longhorn as our storage class, and to claim/create 10 GB and 10B disk of persistent storage. And we will call it longhorn-docker-registry-pvc and longhorn-docker-registry-pvc-cert, by this name we will reference them later.


Notice I have specified namespace, this is important since only pods/deployment in that namespace would be able to see the disk.

To learn more about volumes check out official documentation here:

Apply our pvc.yaml and pvc_cert.yaml

kubectl apply -f pvc.yaml
kubectl apply -f pvc_cert.yaml


I know that you can fit both into one yaml file, I like stuff separated so if I wont to delete one, I just call the appropriate yaml.

And check

root@control01:/home/ubuntu/docker-registry# kubectl get pvc -n docker-registry
NAME                           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
longhorn-docker-registry-pvc   Bound    pvc-39662498-535a-4abd-9153-1c8dfa74749b   10Gi       RWO            longhorn       5d6h

#longhorn should also create automatically PV ( physical volume )
root@control01:/home/ubuntu/docker-registry# kubectl get pv -n docker-registry
NAME                                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
longhorn-docker-registry-pvc        Bound    pvc-700169df-2996-4031-bef5-e4cb7a560264   10Gi       RWO            longhorn       10d
longhorn-docker-registry-pvc-cert   Bound    pvc-520e979f-2531-4b3e-8f0e-be8ee9bf0915   10Mi       RWO            longhorn       28h

Cool cool, now we have storage ! ( Status might be different, something line Unattached )

Creating certificates for docker registry

I literally spend over 24 hours to get this to work with the setup I have, including OpenFaas and so on... so hopefully I did not forget any step :D

Generate certificates in your docker-register directory.

#install opnessl if its not
sudo apt-get install openssl

# generate certificate and key
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout registry.key \
-out registry.crt -subj "/CN=registry.cube.local" \
-addext "subjectAltName=DNS:registry.cube.local,DNS:*.cube.local,IP:"


This is very important, my entry in /etc/hosts for the registry will be registry registry.cube.local. I know what IP it will be as we will set it later ( remember metalLB ?) and there is no DNS server so every node will have to have this in /etc/hosts. Call it what ever you want, but add the correct names into subjectAltName parameters without it there might be issues where some tools complain about not signed properly certificates and missing SAN. Something in sense like: x509: cannot validate certificate for <IP> because it doesn't contain any IP SANs

Now you have two new files in your docker-registry directory

ubuntu@control01:~/docker-registry$ ls | grep regis

We need to tell our upcoming docker registry in kubernetes about certificates and where they are.

Longhorn copy data to disk

We need to copy our certificates to the longhorn disk we created. Remember Longhor UI ? Get to it now. ( I'm not sure how to do it from CLI right now, if you know comment bellow !)

Follow the picture guide. Basically we tell to longhorn to attach the disk to your node ( How freaking cool and simple is that !)

Add data to longhorn storage 1 Add data to longhorn storage 2 Add data to longhorn storage 3 Add data to longhorn storage 4

This will create new VIRTUAL-DISK on the node you attached it.

# in my case its /dev/sdc
Disk /dev/sdc: 10 MiB, 10485760 bytes, 20480 sectors
Disk model: VIRTUAL-DISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

Now create filesystem and mount the volume

mkdir /tmp/disk
# Skipp mkfs if the disk was used before and there are data...
mkfs.ext4 /dev/sdc
mount /dev/sdc /tmp/disk

Copy the certificate and key to /tmp/disk

cp registry.* /tmp/disk
# check
root@control01:/home/ubuntu/docker-registry# ll /tmp/disk
total 32
drwxr-xr-x  3 root root  4096 Feb  1 17:06 ./
drwxrwxrwt 11 root root  4096 Feb  2 22:00 ../
drwx------  2 root root 16384 Feb  1 16:57 lost+found/
-rw-r--r--  1 root root  1907 Feb  2 22:00 registry.crt
-rw-------  1 root root  3272 Feb  2 22:00 registry.key

Unmount the disk and detach in Longhorn UI

umount /tmp/disk

Add data to longhorn storage 5


Now we will create simple deployment of docker registry and let it loose on our Kubernetes cluster.

Create file in your docker-registry directory called docker.yaml

apiVersion: apps/v1
kind: Deployment
  name: registry
  namespace: docker-registry
  replicas: 1
      app: registry
        app: registry
        name: registry
        node-type: worker
      - name: registry
        image: registry:2
        - containerPort: 5000
          value: "/certs/registry.crt"
        - name: REGISTRY_HTTP_TLS_KEY
          value: "/certs/registry.key"
        - name: lv-storage
          mountPath: /var/lib/registry
          subPath: registry
        - name: lv-certs
          mountPath: /certs
        - name: lv-storage
            claimName: longhorn-docker-registry-pvc
        - name: lv-certs
            claimName: longhorn-docker-registry-pvc-cert

What to pay attention to:

  • namespace - I specified docker-registry
  • replicas - I'm using 1, so there will be only one docker-registry running.
  • nodeSelector - As mentioned before in setting up my Kubernetes, I have labeled worker nodes with node-type=worker. This will make so the deployment prefer that nodes.
  • image - This will tell Kubernetes to download registry:2 from official docker hub
  • containerPort - Which port the container will expose/use
  • volumeMounts - Definition of where in the pod we will mount our persistent storage
  • volumes - Definition where we refer back to PVC we created before.
  • env - This will be passed as environmental variables into the container, and used by docker registry.

Apply the deployment and wait a little for everything to come online.

kubectl apply -f docker.yaml

Check with

# Deployment
root@control01:/home/ubuntu/docker-registry# kubectl get deployments -n docker-registry
registry   1/1     1            1           21s
# Pods ( should be 1 )
root@control01:/home/ubuntu/docker-registry# kubectl get pods -n docker-registry
NAME                       READY   STATUS    RESTARTS   AGE
registry-6fdc5fc5d-npslq   1/1     Running   0          29s

We are node done yet, we need to create also service to make the registry available cluster wide and ideally on the same IP/name all the time no matter on what node it runs.


Again, if you followed my network setting I have setup metalLB to provide us with external IP for pods. Therefore we use this as and create LoadBalancer service for our Docker-registry.

In your folder docker-registry create service.yaml and paste in following.

apiVersion: v1
kind: Service
  name: registry-service
  namespace: docker-registry
    app: registry
  type: LoadBalancer
    - name: docker-port
      protocol: TCP
      port: 5000
      targetPort: 5000

What to pay attention to:

  • kind - Servicem, just to let Kubernetes know what we creating.
  • name - Just a name for our service.
  • namespace - I specified docker-registry as the deployment we are targeting is in that name space
  • selector and app - value for this is lifted from our deployment where set this: app: registry
  • type - Here we tell Kubernetes that we want LoadBalancer (MetalLB)
  • ports - we define port on that would be on our exteranl IP and targetPort thats the port inside the app
  • loadBalancerIP - This is optional, but I have included it here, this will allow us to specify which IP we want for the external IP. If you remove that line, MetalLB will assign next free IP from pool we allocated to it.

Apply the service

kubectl apply -f service.yaml

Give it a few second to get the IP and check.

root@control01:/home/ubuntu/docker-registry# kubectl get svc -n docker-registry
NAME               TYPE           CLUSTER-IP   EXTERNAL-IP     PORT(S)          AGE
registry-service   LoadBalancer   5000:32096/TCP   7m48s

Fantastic, service seems to be up and running with external port 5000. About the 32096 port behind it, this can be different for you. It is assigned on node where the pod is running. In essence its like this: External IP:5000 -> Node where the Pod/Container is:32096 -> container inside:5000. I hope that make sense :D

To get more info about the service we can ask Kubectl to describe it to us.

root@control01:/home/ubuntu/docker-registry# kubectl get svc -n docker-registry
NAME               TYPE           CLUSTER-IP   EXTERNAL-IP     PORT(S)          AGE
registry-service   LoadBalancer   5000:32096/TCP   7m48s
root@control01:/home/ubuntu/docker-registry# kubectl describe svc registry-service  -n docker-registry
Name:                     registry-service
Namespace:                docker-registry
Labels:                   <none>
Annotations:              <none>
Selector:                 app=registry
Type:                     LoadBalancer
LoadBalancer Ingress:
Port:                     docker-port  5000/TCP
TargetPort:               5000/TCP
NodePort:                 docker-port  32096/TCP
Session Affinity:         None
External Traffic Policy:  Cluster
  Type    Reason        Age                  From                Message
  ----    ------        ----                 ----                -------
  Normal  IPAllocated   77s (x537 over 11m)  metallb-controller  Assigned IP ""
  Normal  nodeAssigned  76s (x539 over 11m)  metallb-speaker     announcing from node "cube06"

Add root certificate to nodes

Currently we have SSL or TLS, fuck it I will cal it now HTTPS, enabled docker registry, running on kubernetes. However since we create the certificate we need to add it as root certificate to our nodes, every single one ! Or any service that try to use it, will complain about certificate signet by unknown authority or something similar. So we will make our self the authority, this the way !

#For ubuntu
ansible cube -b -m copy -a "src=registry.crt dest=/usr/local/share/ca-certificates/registry.crt"
ansible cube -b -m copy -a "src=registry.key dest=/usr/local/share/ca-certificates/registry.key"
ansible all -b -m shell -a "update-ca-certificates"

In essence on every node you need to copy our registry.crt into /usr/local/share/ca-certificates/ and execute update-ca-certificates which will add our certificate as root certificate. This will fool the verification thinking we are authority for the certificate ( which we are ) and wont complain.

Example of manually doing it:

ubuntu@control01:~/docker-registry$ sudo cp registry.* /usr/local/share/ca-certificates/
ubuntu@control01:~/docker-registry$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...

Making K3s use private docker registry

I know I know, this is taking for ever.

Here is where I got my info from:

Add dns name to /etc/hosts on every node, I named it like this: registry registry.cube.local

Good idea is to have the /etc/hosts nice and synced between all nodes, so I will add it once into control01 node and using Ansible move it to all nodes.

echo " registry registry.cube.local" >> /etc/hosts
ansible cube -b -m copy -a "src=/etc/hosts dest=/etc/hosts"

Now tell k3s about it. As root, create file /etc/rancher/k3s/registries.yaml

nano /etc/rancher/k3s/registries.yaml

Add following:

      - "https://registry.cube.local:5000"
      ca_file: "/usr/local/share/ca-certificates/registry.crt"
      key_file: "/usr/local/share/ca-certificates/registry.key"

Send it to every control node of the cluster.

# Make sure the directory exists
ansible cube -b -m file -a "path=/etc/rancher/k3s state=directory"

# Copy the file
ansible cube -b -m copy -a "src=/etc/rancher/k3s/registries.yaml dest=/etc/rancher/k3s/registries.yaml"

Docker registry test

Follow the guide how to install docker from here:

We will download from official docker registry an Ubuntu container, re-tag it and push to our registry.

root@control01:~# docker pull ubuntu:16.04
16.04: Pulling from library/ubuntu
3e30c5e4609a: Pull complete
be82da0c7e99: Pull complete
bdf04dffef88: Pull complete
2624f7934929: Pull complete
Digest: sha256:3355b6e4ba1b12071ba5fe9742042a2f10b257c908fbdfac81912a16eb463879
Status: Downloaded newer image for ubuntu:16.04

root@control01:~# docker tag ubuntu:16.04 registry.cube.local:5000/my-ubuntu
root@control01:~# docker push
The push refers to repository []
3660514ed6c6: Pushed
2f33c1b8271f: Pushed
753fcdb98fb4: Pushed
1632f6712b3f: Pushed
latest: digest: sha256:2e459e7ec895eb5f94d267fb33ff4d881699dcd6287f27d79df515573cd83d0b size: 1150

# Check with curl
root@control01:~# curl https://registry.cube.local:5000/v2/_catalog

Yay ! it worked

And hopefully this is it, gg getting this far. Now get some coffee and maybe get me one too 🙂

Last update: February 16, 2021