All Articles

Running a private docker registry inside kubernetes

I found this article from Peter Kohut really helpfull in getting this started. However, I wanted to use self singed certificates and also wanted the registry accessible on my virtual “bare-metal” cluster.

Prerequisites

  • A running kubernetes cluster
  • OpenSSL
  • NGINX Ingress Controller for Kubernetes

MySetup

  • Archlinux host with two Ubuntu 18.04.3 qemu guests running a kubernetes master and one node
  • NFS share on the host mounted to /data on both guests
  • Kubernetes version 1.17.0
  • Docker version 18.09.7

Goal

  • Having a private docker registry inside a kubernetes cluster
  • TLS termination in the kubernetes ingress
  • Make the registry available on my host machine
  • Use the registry inside kubernetes

Deploy the registry

I wanted the registry to support basic authentication and persist it on my host machines file system. To persist the data I decided to mount a /data directory via nfs and use hostPath volumes inside the deployment. Another possibility would have been to use the nfs volumes instead. The htpasswd for basic authentication is supplied as a secret.

Create the HTPASSWD secret

First of all use the docker registry image to create the htpasswd file

docker run --entrypoint htpasswd --rm registry:2 -Bbn admin admin | base64

This will give you a base64 encoded string to use in the secret

YWRtaW46JDJ5JDA1JFFleHBwdkJvTlBybHF5YUx5LlZ3V08xVUFuSVV2QU95eTFaSlRDRy95cHMxMDJhaXNpZDZPCgo=

In case you are wondering what is encoded in this string pipe it into base64 -d:

echo YWRtaW46JDJ5JDA1JFFleHBwdkJvTlBybHF5YUx5LlZ3V08xVUFuSVV2QU95eTFaSlRDRy95cHMxMDJhaXNpZDZPCgo= | base64 -d

Now we can put this in our secret to be created:

apiVersion: v1
kind: Secret
metadata:
  name: docker-registry
type: Opaque
data:
  HTPASSWD: YWRtaW46JDJ5JDA1JFFleHBwdkJvTlBybHF5YUx5LlZ3V08xVUFuSVV2QU95eTFaSlRDRy95cHMxMDJhaXNpZDZPCgo=

If you do not want to create the file yourself, feel free to either apply it directly or download it.

kubectl apply -f https://gitlab.com/inw/k8-dockerregistry/raw/master/registry-auth.yaml

Configure the registry

Next create a configMap, I’ve used the one Peter Kohut supplied in his article.

apiVersion: v1
kind: ConfigMap
metadata:
  name: docker-registry
data:
  registry-config.yml: |
    version: 0.1
    log:
      fields:
        service: registry
    storage:
      cache:
        blobdescriptor: inmemory
      filesystem:
        rootdirectory: /var/lib/registry
    http:
      addr: :5000
      headers:
        X-Content-Type-Options: [nosniff]
    auth:
      htpasswd:
        realm: basic-realm
        path: /auth/htpasswd
    health:
      storagedriver:
        enabled: true
        interval: 10s
        threshold: 3

As you can see the registry persists data on the filesystem in /var/lib/registry and uses the file /auth/htpasswd as source for authentication. The registry will listen on port 5000.

If you do not want to create the file yourself, feel free to either apply it directly or download it.

kubectl apply -f https://gitlab.com/inw/k8-dockerregistry/raw/master/registry-config.yaml

Deploy the registry

I’ve decided to use a deployment instead of a pod. However, one replica will be enough.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: docker-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: docker-reg
  template:
    metadata:
      labels:
        app: docker-reg
    spec:
      containers:
        - name: registry
          image: registry:2
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 5000
              protocol: TCP
          volumeMounts:
            - name: config
              mountPath: /etc/docker/registry
              readOnly: true
            - name: htpasswd
              mountPath: /auth
              readOnly: true
            - name: storage
              mountPath: /var/lib/registry            
      volumes:
        - name: config
          configMap:
            name: docker-registry
            items:
              - key: registry-config.yml
                path: config.yml
        - name: htpasswd
          secret:
            secretName: docker-registry
            items:
              - key: HTPASSWD
                path: htpasswd
        - name: storage
          hostPath:
            path: /data/dockerregistry
            type: Directory                       

In the template for the registry container we need to register the volumes and the volume mounts to use. We mount the registry-config.yml key from the configMap at /etc/docker/registry/config.yml and the docker-registry secret with the key HTPASSWD at /auth/htpasswd. Both files are mounted read only. Last but not least we bind /var/lib/registry to the host path /data/dockerregistry. If you want to use another volume type check out “Types of Volumes” in the official kubernetes documentation.

If you do not want to create the file yourself, feel free to either apply it directly or download it.

kubectl apply -f https://gitlab.com/inw/k8-dockerregistry/raw/master/registry-deployment.yaml

The registry pod shold now up an running. Make sure to check it with

kubectl get po

If you experience problems check out the troubleshooting section.

Exposing the registry

Now its time to expose the registry as a service with a ClusterIP. This is quite straightforward.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: docker-registry
  name: docker-registry
spec:
  ports:
  - name: http
    port: 5000
    protocol: TCP
    targetPort: 5000
  selector:
    app: docker-reg
  type: ClusterIP

If you do not want to create the file yourself, feel free to either apply it directly or download it.

kubectl apply -f https://gitlab.com/inw/k8-dockerregistry/raw/master/registry-service.yaml

If you like verify the service has endpoints by running kubectl get ep $servicename.

Creating the ingress with TLS termination

I’m using NGINX Ingress Controller for Kubernetes without ssl passthrough. Please refer to the installation guide for deploying the controller.

For all examples the domain registry.k8 is used.

Create the certificate and the secret

Use openssl to create a self signed certificate and a key:

openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out registry.crt -keyout registry.key

Make sure to supply registry.k8 as common name.

Now create a TLS-Secret for kubernetes by running

kubectl create secret tls registry-tls --key registry.key --cert registry.crt

as described here.

Configuring the ingress

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: docker-registry
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: 0m
spec:
  tls:
  - hosts:
    - registry.k8
    secretName: registry-tls
  rules:
  - host: registry.k8
    http:
      paths:
      - backend:
          serviceName: docker-registry
          servicePort: 5000

This ingress will be listening on ports 80 and 443 and send all traffic for registry.k8 to the docker-registry service on port 5000 - as configured in the configMap for the registry. The annotation nginx.ingress.kubernetes.io/proxy-body-size: 0m is needed to disable the max body size of the nginx ingress. Otherwise we would not be able to push docker images - or at least only very small ones.

If you do not want to create the file yourself, feel free to either apply it directly or download it.

kubectl apply -f https://gitlab.com/inw/k8-dockerregistry/raw/master/registry-ingress.yaml

Now run

kubectl get ing docker-registry 

and note the ClusterIP of the ingress.

Configuring a reverse proxy

To get traffic into my cluster I’m using my master-node. Therefore I’ve added an entry to my /etc/hosts file containing registry.k8 $masternodeIP. Incomming traffic on my master-node is then handled by an nginx reverse proxy as a stream. Therefore I not have to worry about TLS termination on the reverse proxy.

To make this work edit the /etc/nginx and add include /etc/nginx/streams-enabled/*; as a top level entry. Streams have to be configured on the same level as http or mail. Now create a file named /etc/nginx/sites-available/reverse-proxy-stream.conf with the content of

error_log /var/log/nginx/error.log;

stream {
  
  upstream registry {
   server $INGRESS_CLUSTER_IP:443 max_fails=10 fail_timeout=10s;
  }  

  server {
        listen 5000;
        listen [::]:5000;

        proxy_pass registry;
  }
}

and link it into include /etc/nginx/streams-enabled/*;, run sudo nginx -t to verify your configuration an then restart the nginx service. Make sure to replace $INGRESSCLUSTERIP with the one you discovered above.

Now you should be able to open (https://registry.k8:5000/v2/_catalog) and authenticate with admin:admin - or whatever username and password you have chosen. Of course you can use whatever port you want the reverse proxy to listen to. Just make sure to remember it for the next and (nearly) last step. If you cannot reach the registry checkout the troubleshooting section.

Tell docker to trust the self signed certificates

Last but not least we must tell docker to trust our self signed certificates. On each docker installation you want to use your registry run

sudo mkdir -p /etc/docker/certs.d/registry.k8:5000
sudo cp registry.crt /etc/docker/certs.d/registry.k8\:5000/.

Docker will now trust your certificate for registry.k8:5000.

Login to your registry

docker login -u admin -p admin registry.k8:5000

Tag an image and push it

docker image tag $imagetopush registry.k8:5000/$imagetopush
docker push registry.k8:5000/$imagetopush

This should work. If not, refer to the troubleshooting section.

Tell kubernetes to use the registry

Wouldn’t it be nice to use your registry from inside your kubernetes cluster as well? Actually thats quite straightforward.

  • Make sure every node knows to point to your master-node when asked for registry.k8
  • Create a docker login secret kubectl create secret docker-registry regcred --docker-server=registry.k8:5000 --docker-username=admin --docker-password=admin
  • Use following container template:

    containers:
    - name: mycontainer
    image: registry.k8:5000/myimage
    imagePullSecrets:
    - name: regcred

    Further information can be found in the official documentation.

Troubleshooting

Registry pod has problems

  • check with kubectl describe po $podname for detailed errors.
  • check with kubectl logs $podname for detailed errors.

Most of the times I had a typo in a some config files misplacing the secret or the volumeMounts. Also some filesystem permissions may to be set to 777. There seems to be a problem with the umask in some cases using hostPath volumes. Just make sure the container is allowed to write to the host path.

Registry is not reachable from a browser

First check if your /etc/hosts lists registry.k8 and points to your kubernetes master and your registry pod is running. If this is the case check if your reverse proxy is configured correctly, e.g. points to the correct ClusterIP of the ingress. The next step is to check your ingress configuration, the service should point to your registry pod as endpoint. A very hand command to check if you can reach the registry is curl https://IP_ADDRESS:PORT/v2/_catalog -k. Just replace the IP_ADDRESS:PORT with either the domain name and port or the IP address/port combination of your ingress, service or pod. You should always get a JSON reply stating an unauthorized error. Just make sure you are on your kubernetes master if you check ingress, service or pod. Remember to use kubectl describe ... to get further information about a kubernetes resource.

Cannot push to the registry

Cannot connect

Make sure you have copied the certificate to /etc/docker/conf.d/$registry_domain:port/.. Next make sure the CN of the certificate is set to the registry domain by running openssl x509 -in certificate.crt -text -noout | grep CN. Point your browser to https://registry.k8:5000/v2/_catalog (or whatever you are using as registry domain) and check if the correct certificate is used. If this is not the case double check your ingress configuration and the used TLS secret.

Cannot upload

Make sure your ingress and reverse proxy do not limit the body size. Also make sure you tagged your image correctly. Do not forget to add the port, as I did first.