k8s-nextjs-deploy
Kubernetes deployment patterns for containerized Next.js apps: Harbor registry auth, Traefik ingress with automatic TLS, Linkerd sidecar injection, and multi-app namespace management.
When to use
- Deploying a new Next.js app (Deployment + Service + Ingress rule)
- Diagnosing
ImagePullBackOff,ErrImagePull,CreateContainerConfigError - Rotating Harbor pull secret credentials
- Adding a subdomain to an existing Traefik ingress with cert-manager
- Applying K8s manifests after a namespace was deleted
- Troubleshooting pods stuck because of missing
secretKeyRefsecrets
Do NOT use for:
- Non-Kubernetes deployments (Docker Compose, bare-metal, Vercel)
- Database deployments (stateful sets require separate skill)
Deploying a new Next.js app — step by step
- Create the namespace (if it doesn't exist yet):
kubectl create namespace <ns>. Success:kubectl get ns <ns>showsActive. - Create the Harbor pull secret using the single-quoted
docker-registrycommand below. Success:kubectl -n <ns> get secret harbor-pull-secretexists anddescribeshows typekubernetes.io/dockerconfigjson. - Apply the Deployment manifest (with
imagePullSecrets, resource requests/limits, and readiness/liveness probes set). Success:kubectl -n <ns> get podsshows the podRunningand1/1 Ready. - Apply the Service. Success:
kubectl -n <ns> get svc <name>shows a ClusterIP with the expected port mapping. - Apply/extend the Ingress, adding the new host to both
tls.hostsandrules. Success:kubectl -n <ns> get ingresslists the host, andkubectl -n <ns> get certificatereachesREADY: Truewithin a couple of minutes as cert-manager issues the cert. - Verify end-to-end:
curl -I https://<host>returns200/3xxwith a trusted TLS chain (no cert warning). If it doesn't, work through Common failure diagnosis below.
Deployment manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudreviewer-web
namespace: cloudreviewer
spec:
replicas: 1
selector:
matchLabels:
app: cloudreviewer-web
template:
metadata:
labels:
app: cloudreviewer-web
spec:
imagePullSecrets:
- name: harbor-pull-secret
containers:
- name: web
image: harbor.example.com/project/app:latest
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
ports:
- containerPort: 3000
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
env:
- name: HOSTNAME
value: "0.0.0.0"
- name: NODE_ENV
value: "production"
- name: NEXT_TELEMETRY_DISABLED
value: "1"
- name: OTEL_SERVICE_NAME
value: "my-app"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://tempo.tempo.svc.cluster.local:4318"
readinessProbe:
httpGet:
path: /
port: 3000
livenessProbe:
httpGet:
path: /
port: 3000
Service
apiVersion: v1
kind: Service
metadata:
name: cloudreviewer-web
namespace: cloudreviewer
spec:
selector:
app: cloudreviewer-web
ports:
- name: http
port: 80
targetPort: 3000
Ingress (Traefik + cert-manager)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cloudreviewer
namespace: cloudreviewer
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- www.example.com
- buy.example.com
- kb.example.com
secretName: cloudreviewer-tls
rules:
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cloudreviewer-web
port:
number: 80
Add new subdomains to both tls.hosts and rules.
Harbor pull secret
kubectl -n <namespace> create secret docker-registry harbor-pull-secret \
--docker-server=harbor.example.com \
--docker-username='robot$project+robot' \
--docker-password='TOKEN'
Single quotes are mandatory — robot account usernames contain $ which the shell would expand. Never use double quotes.
Rotating credentials
kubectl -n <namespace> delete secret harbor-pull-secret
kubectl -n <namespace> create secret docker-registry harbor-pull-secret \
--docker-server=harbor.example.com \
--docker-username='robot$project+robot' \
--docker-password='NEW_TOKEN'
kubectl -n <namespace> rollout restart deployment/<name>
Pods scheduled before the secret existed cache the auth failure. Always rollout restart after recreating the secret.
Common failure diagnosis
ImagePullBackOff / ErrImagePull
kubectl -n <ns> describe pod <pod> | grep -A 10 "Events:"
| Error message | Cause | Fix |
|---|---|---|
401 Unauthorized |
Stale or missing pull secret | Rotate pull secret → rollout restart |
repository does not exist |
Wrong image name or project | Check Harbor project name matches image path |
no basic auth credentials |
Pod scheduled before secret created | rollout restart after creating secret |
CreateContainerConfigError
Pod image pulled successfully but container won't start — usually a missing secretKeyRef:
kubectl -n <ns> describe pod <pod> | grep -i "secret\|error" | head -20
The referenced secret (e.g., cloudreviewer-buy-secrets) was lost when the namespace was deleted. Recreate it:
kubectl -n <ns> create secret generic my-app-secrets \
--from-literal=key1=value1 \
--from-literal=key2=value2
Namespace deleted — full apply order
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/web-deployment.yaml -f k8s/web-service.yaml
kubectl apply -f k8s/buy-deployment.yaml -f k8s/buy-service.yaml
kubectl apply -f k8s/kb-deployment.yaml -f k8s/kb-service.yaml
kubectl apply -f k8s/ingress.yaml
# Then recreate pull secret and app secrets
Environment variables for Next.js
Required env vars for a standalone Next.js container:
| Variable | Value | Purpose |
|---|---|---|
HOSTNAME |
0.0.0.0 |
Bind to all interfaces |
NODE_ENV |
production |
Enables prod optimisations |
NEXT_TELEMETRY_DISABLED |
1 |
Disable Next.js telemetry |
OTEL_SERVICE_NAME |
app name | OTel trace attribution |
OTEL_EXPORTER_OTLP_ENDPOINT |
Tempo/Grafana URL | Trace export |
NEXT_PUBLIC_SITE_URL |
https://www.example.com |
Metadata base URL |
Multiple kubectl contexts
When managing multiple clusters:
kubectl config get-contexts # list
kubectl config use-context sr-k8s # switch
kubectl config current-context # verify
Always verify context before applying manifests or rotating secrets.
Example prompts
- "Our pods are stuck in
ImagePullBackOff. How do I diagnose and fix this?" - "I rotated the Harbor robot account token. How do I update the pull secret without downtime?"
- "Add subdomain
kb.example.comto the existing Traefik ingress." - "The namespace got accidentally deleted. Walk me through restoring everything in order."
- "A pod is in
CreateContainerConfigError. What does that mean and how do I fix it?" - "Show me a production-ready Next.js Deployment manifest with resource limits and probes."
Related skills
nextjs-monorepo-ci— CI pipeline that builds and pushes the images this skill deploysarcgis-enterprise-k8s— deploying a more complex stateful app on Kubernetes