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.
First, the accessMode
of 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.
Secondly, 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!
Deployment
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.
Service
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.
Ingress
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.