Private Docker Registry in K8s
Mirroring package repositories has been an option available to Linux users for a long time. It’s a great way to save bandwidth and speed up package installation. This is especially true if you’re using Kubernetes, where you’ll be pulling images from a registry many times a day. There’s a lot of value to doing the same with Docker images, particularly for any that are private and only in active use in your homelab.
In this post, we’ll explore some of the considerations for setting up a private Docker registry in Kubernetes.
tl;dr - Here Are the Manifests
For those of you looking for example manifests instead of an explanation of the process, here you go. This will create the Persistent Volume Claim (PVC) that allows you to store the registry data on a persistent volume, the Deployment that runs the registry, the Service that exposes it, and the Ingress that allows you to access it from outside the cluster. Note that I’m using the
nfs-csi-retain StorageClass, which uses a CSI driver for NFS that I’ve configured to retain the volume when the PVC is deleted. I’ve also set up Ingress to use cert-manager to automatically provision a TLS certificate from LetsEncrypt and nginx to handle the routing/load balancing. Those are out of the scope of this post, but I’ll cover them in a future post. If you want to set them up on your own I used the Helm charts for both.
You’ll want to change the namespace to something more appropriate for you and include a manifest to create that namespace, if it doesn’t already exist. Copying and pasting these manifests and running
kubectl apply will probably not work!
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: docker-registry-pvc namespace: graywind spec: storageClassName: nfs-csi-retain accessModes: [ReadWriteMany] resources: requests: storage: 50Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: docker-registry-deployment namespace: graywind spec: replicas: 1 selector: matchLabels: app: docker-registry template: metadata: labels: app: docker-registry spec: containers: - name: docker-registry image: registry:2 ports: - containerPort: 5000 volumeMounts: - name: docker-registry-app-data mountPath: /var/lib/registry subPath: registry volumes: - name: docker-registry-app-data persistentVolumeClaim: claimName: docker-registry-pvc --- apiVersion: v1 kind: Service metadata: name: docker-registry-service namespace: graywind spec: selector: app: docker-registry ports: - protocol: TCP port: 5000 targetPort: 5000 nodePort: 30500 type: NodePort --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: docker-registry-ingress namespace: graywind annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/proxy-body-size: "2.5G" spec: ingressClassName: nginx rules: - host: docker.graywind.org http: paths: - path: / pathType: Prefix backend: service: name: docker-registry-service port: number: 5000 tls: - hosts: - docker.graywind.org # This resolves only in my private DNS, so I know it won't be used outside my network secretName: docker-registry-tls
Persistent Volume Claim (PVC)
Previously, I set up a Persistent Volume called
nfs-csi-retain that uses NFS to store data on a Synology NAS. I’m using that same volume to store the data for the Docker registry. The PVC is pretty straightforward, but there are a few things to note.
ReadWriteMany is not required - you could just as easily use
ReadWriteOnce - but I set it up this way so I could have zero downtime when I need to update the registry or if the machine running it is rebooted or loses power.
spec.resources.request.storage is key to making sure that the PVC is large enough to store the data. I’m using 50Gi, but you can use whatever size you need. If you don’t set this, Kubernetes will create a PVC that is only 1Gi in size, which is not enough for a Docker registry. Sometimes it’s not enough for a single image!
The Deployment is also pretty straightforward. Let’s look at a few of the key points. First, I’m using the
app: docker-registry label to make it easier to select the pod for debugging, setting up a Service, or other purposes. I’m mounting the volume we set up in the first manifest to
/var/lib/registry. Finally, there’s the image itself -
registry:2, which is also
latest as of this writing. You can use whatever version you want, but I recommend using a specific version instead of
latest so you don’t accidentally upgrade to a version that breaks something.
The Service is a key piece of the infrastructure. Without it, you won’t be able to access the registry from outside the cluster. I’m using a NodePort Service, which means that the registry will be accessible on every node in the cluster on port 30500.
Finally, the Ingress is what allows us to communicate securely with the Docker Registry. Docker expects any registry to be using HTTPS, so this is a key piece of the infrastructure. We’re using two annotations. One is for LetsEncrypt, which will automatically provision a TLS certificate for us. The other is for nginx, which is the Ingress controller I’m using. It’s important to set the
nginx.ingress.kubernetes.io/proxy-body-size annotation to a large enough value to allow you to push large images to the registry. I’m using 2.5G, which should be plenty for most Docker images I’ll be using. The
spec.rules section allows us to forward traffic to the Service appropriately.
Why do all this?
Why go to all this trouble with Kubernetes, nginx, LetsEncrypt, and the PVC? I originally tried to run a very simple container registry on my Synology NAS, but almost immediately ran into issues with the lack of TLS encryption. The registry itself worked just fine and the volume it was using did too. However, Docker expects that registries are encrypted, and setting it up to accept a registry over HTTP instead of HTTPS is a bit of a pain.
I initially experimented with this using Nomad. While I could easily run the container, the community support for container storage is not as strong, and neither is the support for TLS. I could have used a self-signed certificate, but I wanted to use a real certificate from LetsEncrypt. I could have done all of that with Nomad, but it would have been a lot more work.
Kubernetes isn’t always the right solution, but for my situation, it turned out to be the least hassle to get a working container registry.
If you have questions or issues setting up a similar registry, please feel free to reach out! I’m happy to chat through what’s going on and help get you up and running.