K3s Private Docker Registry
Self-hosted Docker registry on K3s using the official registry:2 image, secured with htpasswd authentication, a TLS certificate via cert-manager, and restricted to the WireGuard VPN and internal cluster networks via a Traefik IP allowlist.
Prerequisites
- K3s running with Traefik and cert-manager configured (see the K3s Setup article)
- A
cloudflare-cluster-issuerClusterIssuer
1. Create Auth Secret
kubectl create namespace registry
# Install htpasswd if needed
sudo apt install apache2-utils # Debian/Ubuntu
# or: sudo dnf install httpd-tools # Rocky Linux
# Generate bcrypt credentials
htpasswd -Bc /tmp/htpasswd admin
# Create the secret
kubectl create secret generic registry-auth \
--from-file=htpasswd=/tmp/htpasswd \
-n registry
2. Deploy Registry
The registry uses a 50 Gi PVC for image storage and mounts the htpasswd secret for authentication.
registry.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: registry
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-pvc
namespace: registry
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry
namespace: 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_STORAGE_FILESYSTEM_ROOTDIRECTORY
value: /data
- name: REGISTRY_AUTH
value: htpasswd
- name: REGISTRY_AUTH_HTPASSWD_REALM
value: Registry
- name: REGISTRY_AUTH_HTPASSWD_PATH
value: /auth/htpasswd
volumeMounts:
- name: storage
mountPath: /data
- name: auth
mountPath: /auth
volumes:
- name: storage
persistentVolumeClaim:
claimName: registry-pvc
- name: auth
secret:
secretName: registry-auth
---
apiVersion: v1
kind: Service
metadata:
name: registry
namespace: registry
spec:
selector:
app: registry
ports:
- port: 5000
targetPort: 5000
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: registry-cert
namespace: registry
spec:
secretName: registry-tls
issuerRef:
name: cloudflare-cluster-issuer
kind: ClusterIssuer
dnsNames:
- registry.webux.dev
---
# Restrict access to WireGuard + k3s pod/service networks
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: registry-allowlist
namespace: registry
spec:
ipAllowList:
sourceRange:
- 10.11.0.0/24 # WireGuard
- 10.42.0.0/16 # k3s pod CIDR
- 10.43.0.0/16 # k3s service CIDR
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: registry
namespace: registry
spec:
entryPoints:
- websecure
routes:
- match: Host(`registry.webux.dev`)
kind: Rule
middlewares:
- name: registry-allowlist
namespace: registry
services:
- name: registry
port: 5000
tls:
secretName: registry-tls
kubectl apply -f registry.yaml
# Watch certificate get issued (~1-2 min)
kubectl get certificate -n registry -w
3. Configure K3s to Trust the Registry
Create registries.yaml with your credentials and copy it to every node. K3s reads this file on startup and generates the containerd hosts.toml internally — do not place hosts.toml manually, it gets wiped on restart.
registries.yaml
mirrors:
"registry.webux.dev":
endpoint:
- "https://registry.webux.dev"
configs:
"registry.webux.dev":
auth:
username: admin
password: YOUR_REGISTRY_PASSWORD
tls:
insecure_skip_verify: false
sudo cp registries.yaml /etc/rancher/k3s/registries.yaml
sudo systemctl restart k3s
# Verify
sudo k3s crictl info | jq '.config.registry'
4. Push Images
# Login
docker login registry.webux.dev
# Tag and push
docker build -t registry.webux.dev/my-app:latest .
docker push registry.webux.dev/my-app:latest
5. Use in Kubernetes Manifests
No imagePullSecret needed — k3s handles auth transparently via registries.yaml.
containers:
- name: my-app
image: registry.webux.dev/my-app:latest