Kubernetes Deployment with probes and zero-downtime rollouts
A production-ready baseline for a stateless HTTP service: readiness, liveness and startup probes, resource limits, a non-root security context and rolling updates that never drop capacity.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels: { app: api }
spec:
replicas: 3
selector:
matchLabels: { app: api }
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
template:
metadata:
labels: { app: api }
spec:
terminationGracePeriodSeconds: 30
securityContext: { runAsNonRoot: true, runAsUser: 1001 }
containers:
- name: api
image: ghcr.io/acme/api:sha-1a2b3c4
ports: [{ name: http, containerPort: 3000 }]
envFrom: [{ secretRef: { name: api-env } }]
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 512Mi }
startupProbe: # gives slow boots time before liveness kicks in
httpGet: { path: /healthz, port: http }
periodSeconds: 5
failureThreshold: 24
readinessProbe: # controls whether the pod receives traffic
httpGet: { path: /readyz, port: http }
periodSeconds: 5
failureThreshold: 2
livenessProbe: # restarts the container if the process is stuck
httpGet: { path: /healthz, port: http }
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop: # let endpoints update before SIGTERM arrives
sleep: { seconds: 5 }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector: { app: api }
ports: [{ name: http, port: 80, targetPort: http }]How it works & gotchas
- 01Readiness decides traffic, liveness decides restarts. Liveness should only check that the process responds, never the database, or one DB blip restarts every pod at once.
- 02
maxUnavailable: 0withmaxSurge: 1keeps full capacity during a rollout: a new pod must be ready before an old one is removed. - 03Only a memory limit is set. CPU limits cause throttling and latency spikes; a CPU request is enough for scheduling.
- 04The native
preStop.sleepaction needs Kubernetes 1.30+. On older clusters useexec: { command: ["sleep", "5"] }, which requires a shell in the image. - 05
readOnlyRootFilesystembreaks apps that write to disk. Mount anemptyDirat/tmpif yours does.
Need this wired into a real system?
We build and harden production software end to end.
More code helpers
Multi-stage Dockerfile for Next.js standalone output
Build a small production image for a Next.js app that runs as a non-root user and contains only the traced server files, not the full node_modules.
GitHub Actions CI: lint, test, build and push to GHCR
Run checks on every pull request, and on main build a Docker image and push it to GitHub Container Registry with layer caching. No long-lived registry credentials needed.
Stream LLM responses from a Next.js route handler
Send tokens to the browser as the model produces them instead of making users wait for the full completion. Uses a plain ReadableStream, so it works with any client that can read a fetch body.