Host Your Private Docker Registry on Kubernetes
CKA prep • Run registry:2 in-cluster with PVC persistence, TLS, htpasswd auth, imagePullSecrets
Key terms
| Term | Meaning |
|---|---|
| registry:2 | The official Docker Registry (Distribution) image |
| PVC | PersistentVolumeClaim that keeps pushed images across restarts |
| htpasswd | Basic-auth user file the registry reads for login |
| TLS Secret | kubernetes.io/tls cert + key so pushes use HTTPS |
| imagePullSecret | docker-registry Secret a pod uses to pull private images |
| insecure registry | A registry served over plain HTTP (must be allow-listed) |
| /v2/_catalog | Registry API endpoint that lists repositories |
Problem & solution
Public registries cost money, rate-limit pulls, and may not be allowed for
internal images. Running your own registry inside the cluster gives you a
private, fast image store — but a bare registry:2 pod loses every image on
restart and accepts anonymous pushes. You need persistence, TLS, and
authentication to make it real.
Solution: Deploy registry:2 backed by a PVC, front it with a TLS Secret and an htpasswd auth file, then let pods pull from it via a docker-registry imagePullSecret.
The analogy
At the port, instead of fetching every cargo design from the crowded public dock, you build your own private bonded warehouse that stores your container blueprints under lock and key. A guard at the gate checks papers before anyone may deposit or collect a blueprint, and the warehouse keeps everything on durable storage racks so nothing is lost when the lights go out. A ship may collect a blueprint only if it carries the right warehouse pass. In Kubernetes that warehouse is your private registry, the blueprints are container images, the guard is TLS plus htpasswd auth, the racks are a PVC, and the pass is an imagePullSecret.
Architecture at a glance
Create the auth and TLS secrets
The registry reads a bcrypt htpasswd file for basic auth and a tls Secret for HTTPS.
# 1) htpasswd file (bcrypt) -> a generic Secret
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin S3cret! > htpasswd
kubectl create secret generic registry-auth --from-file=htpasswd
# 2) a TLS cert for the registry's DNS name (self-signed for a lab)
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout tls.key -out tls.crt \
-subj "/CN=registry.default.svc" \
-addext "subjectAltName=DNS:registry.default.svc"
kubectl create secret tls registry-tls --cert=tls.crt --key=tls.key
Persist images with a PVC
A registry that writes to the container filesystem loses every image when the pod restarts. Back it with a PersistentVolumeClaim so pushed images survive:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Deploy registry:2
This Deployment runs the registry image and wires in the auth, TLS, and storage you just created through env vars and volume mounts:
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry
labels: { app: registry }
spec:
replicas: 1
selector:
matchLabels: { app: registry }
template:
metadata:
labels: { app: registry }
spec:
containers:
- name: registry
image: registry:2
ports:
- containerPort: 5000
env:
- name: REGISTRY_AUTH
value: htpasswd
- name: REGISTRY_AUTH_HTPASSWD_REALM
value: "Registry Realm"
- name: REGISTRY_AUTH_HTPASSWD_PATH
value: /auth/htpasswd
- name: REGISTRY_HTTP_TLS_CERTIFICATE
value: /certs/tls.crt
- name: REGISTRY_HTTP_TLS_KEY
value: /certs/tls.key
volumeMounts:
- { name: data, mountPath: /var/lib/registry }
- { name: auth, mountPath: /auth, readOnly: true }
- { name: certs, mountPath: /certs, readOnly: true }
volumes:
- name: data
persistentVolumeClaim: { claimName: registry-data }
- name: auth
secret: { secretName: registry-auth }
- name: certs
secret: { secretName: registry-tls }
Expose it with a Service
Give the registry a stable in-cluster address so clients and pods can reach it by name:
apiVersion: v1
kind: Service
metadata:
name: registry
spec:
selector: { app: registry }
ports:
- port: 5000
targetPort: 5000
In-cluster, the registry is reachable at registry.default.svc:5000. For
node-level docker/crictl access, expose a NodePort or LoadBalancer instead.
Push and pull images
With the registry running, you log in over TLS, then tag and push an image and read it back from the catalog:
# log in (basic auth over TLS)
docker login registry.default.svc:5000 -u admin -p 'S3cret!'
# tag, push, then list the catalog
docker tag nginx:latest registry.default.svc:5000/team/nginx:1.0
docker push registry.default.svc:5000/team/nginx:1.0
curl -u admin:'S3cret!' https://registry.default.svc:5000/v2/_catalog
docker pull registry.default.svc:5000/team/nginx:1.0
Let pods pull private images
Create a docker-registry Secret and reference it from the pod spec (or attach it
to a ServiceAccount so every pod inherits it).
kubectl create secret docker-registry regcred \
--docker-server=registry.default.svc:5000 \
--docker-username=admin \
--docker-password='S3cret!'
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
imagePullSecrets:
- name: regcred
containers:
- name: web
image: registry.default.svc:5000/team/nginx:1.0
End-to-end: an image from push to running pod
End-to-end example: private registry from zero to a running Pod
A complete, runnable walkthrough: create the secrets, deploy a persistent registry over TLS, push a real image, then pull it into a Pod through an imagePullSecret and confirm the container is Running.
- Create a namespace and the htpasswd auth secret (bcrypt):
kubectl create namespace registry
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin 'S3cret!' > htpasswd
kubectl -n registry create secret generic registry-auth --from-file=htpasswd
- Mint a self-signed TLS cert whose SAN matches the in-cluster DNS name and load it as a tls Secret:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout tls.key -out tls.crt \
-subj "/CN=registry.registry.svc" \
-addext "subjectAltName=DNS:registry.registry.svc"
kubectl -n registry create secret tls registry-tls --cert=tls.crt --key=tls.key
- Apply the PVC, Deployment, and Service in one manifest:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-data
namespace: registry
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry
namespace: registry
labels: { app: registry }
spec:
replicas: 1
selector:
matchLabels: { app: registry }
template:
metadata:
labels: { app: registry }
spec:
containers:
- name: registry
image: registry:2
ports: [{ containerPort: 5000 }]
env:
- { name: REGISTRY_AUTH, value: htpasswd }
- { name: REGISTRY_AUTH_HTPASSWD_REALM, value: "Registry Realm" }
- { name: REGISTRY_AUTH_HTPASSWD_PATH, value: /auth/htpasswd }
- { name: REGISTRY_HTTP_TLS_CERTIFICATE, value: /certs/tls.crt }
- { name: REGISTRY_HTTP_TLS_KEY, value: /certs/tls.key }
volumeMounts:
- { name: data, mountPath: /var/lib/registry }
- { name: auth, mountPath: /auth, readOnly: true }
- { name: certs, mountPath: /certs, readOnly: true }
volumes:
- { name: data, persistentVolumeClaim: { claimName: registry-data } }
- { name: auth, secret: { secretName: registry-auth } }
- { name: certs, secret: { secretName: registry-tls } }
---
apiVersion: v1
kind: Service
metadata:
name: registry
namespace: registry
spec:
selector: { app: registry }
ports: [{ port: 5000, targetPort: 5000 }]
- Wait for the rollout and confirm the registry answers its API:
kubectl -n registry rollout status deploy/registry
# expected: deployment "registry" successfully rolled out
kubectl -n registry run probe --rm -it --restart=Never --image=curlimages/curl -- \
curl -sk -u admin:'S3cret!' https://registry.registry.svc:5000/v2/_catalog
# expected: {"repositories":[]}
- From a node (or a build host that can reach the Service), log in, tag, and push:
docker login registry.registry.svc:5000 -u admin -p 'S3cret!'
# expected: Login Succeeded
docker tag nginx:1.27 registry.registry.svc:5000/team/nginx:1.0
docker push registry.registry.svc:5000/team/nginx:1.0
# expected: 1.0: digest: sha256:... size: ...
- Create the docker-registry pull secret and reference it from a Pod:
kubectl create namespace app
kubectl -n app create secret docker-registry regcred \
--docker-server=registry.registry.svc:5000 \
--docker-username=admin \
--docker-password='S3cret!'
apiVersion: v1
kind: Pod
metadata:
name: web
namespace: app
spec:
imagePullSecrets:
- name: regcred
containers:
- name: web
image: registry.registry.svc:5000/team/nginx:1.0
- Verify the private image was pulled and the container is Running:
kubectl -n app get pod web -o wide
# expected: web 1/1 Running
kubectl -n app describe pod web | grep -iE "pulled|pulling"
# expected: Successfully pulled image "registry.registry.svc:5000/team/nginx:1.0"
- Prove persistence: delete the registry pod and re-list the catalog:
kubectl -n registry delete pod -l app=registry
kubectl -n registry rollout status deploy/registry
kubectl -n registry run probe --rm -it --restart=Never --image=curlimages/curl -- \
curl -sk -u admin:'S3cret!' https://registry.registry.svc:5000/v2/_catalog
# expected: {"repositories":["team/nginx"]} (survived the restart via the PVC)
Common pitfalls
These are the failures you are most likely to hit, with the usual cause for each:
- images vanish on restart -> no PVC; registry wrote to the container fs
- x509 / cert errors on push -> SAN must include the name you push to
- http: server gave HTTPS? -> client used plain HTTP to a TLS registry
- ErrImagePull on private img -> missing/wrong imagePullSecret on the pod
- login works, pull fails -> kubelet has no creds; use a docker-registry Secret
- want plain HTTP -> add the host to containerd insecure registries
Key takeaways
registry:2is the image; persist/var/lib/registrywith a PVC or lose images.- Secure it with an htpasswd Secret (auth) and a tls Secret (HTTPS).
- The cert's SAN must match the name clients push/pull with.
- Pods pull private images via a
docker-registryimagePullSecret. - Attach the pull Secret to a ServiceAccount to apply it cluster-wide.
GET /v2/_cataloglists repositories to confirm a push landed.
Checklist
- [ ] Created htpasswd and tls Secrets for the registry
- [ ] Backed the registry with a PVC and confirmed images survive a restart
- [ ] Deployed registry:2 with auth + TLS env vars and a Service
- [ ] Pushed an image and listed it via
/v2/_catalog - [ ] Created a
docker-registrySecret and pulled a private image into a pod