Docker to Kubernetes: Migration Path and Production Patterns (2026)
Docker Compose is perfect for a single host — one machine, a handful of services, straightforward networking. Kubernetes becomes necessary when you need multi-host scheduling, automatic failover, horizontal pod autoscaling, rolling deployments without downtime, and declarative self-healing infrastructure. The mental model shift is significant: services become Deployments, ports become Services, env vars become ConfigMaps and Secrets, volumes become PersistentVolumeClaims. This phase maps each Compose concept to its K8s equivalent and covers the production patterns that matter most when migrating.
Table of Contents
When to Migrate
# Stay with Docker Compose when:
# ✅ Running on a single server
# ✅ Team is small (1–5 engineers)
# ✅ Traffic is predictable and moderate
# ✅ Simple deployment story is valued over operational complexity
# ✅ Budget doesn't support managed K8s (EKS ~$73/mo minimum)
# ✅ App doesn't need multi-AZ failover
# Move to Kubernetes when:
# 🚀 Multiple hosts needed (traffic exceeds single-server capacity)
# 🚀 Need automatic failover (pod dies → K8s reschedules it instantly)
# 🚀 Need horizontal autoscaling (HPA scales replicas on CPU/RPS)
# 🚀 Zero-downtime rolling deployments with automatic rollback
# 🚀 Multiple teams deploying independently to the same cluster
# 🚀 Complex service mesh, canary deployments, A/B traffic splitting
# 🚀 Compliance requires multi-AZ, RBAC, audit logs at platform level
# Middle ground: managed container services
# - AWS ECS Fargate: Docker Compose-like, no node management
# - Google Cloud Run: container → URL, scales to zero
# - Railway / Render / Fly.io: Compose-adjacent, low ops overhead
# These are often the right step between single Docker host and full K8s.
Concept Mapping
# Docker Compose → Kubernetes equivalents:
#
# Compose K8s Notes
# ─────────────────────────── ───────────────────────── ──────────────────────────
# service: Deployment Stateless app (web, api)
# StatefulSet Stateful (DB, Redis, Kafka)
# DaemonSet One pod per node (log agent)
# Job / CronJob One-shot / scheduled tasks
#
# image: spec.containers[].image Same format
# ports: Service (ClusterIP/LB) Separate object, not in Pod
# environment: ConfigMap / env Non-secret config
# env_file: secrets Secret base64-encoded, RBAC-gated
# volumes: named PersistentVolumeClaim Dynamic provisioning
# volumes: bind hostPath (avoid in prod) Or use ConfigMap for configs
# networks: Namespace / NetworkPolicy Built-in DNS between pods
# depends_on: initContainers Or just let app retry
# healthcheck: livenessProbe Restart unhealthy pods
# readinessProbe Remove from Service LB
# startupProbe Grace period at startup
# restart: unless-stopped restartPolicy: Always Default for Deployment pods
# deploy.replicas: 3 spec.replicas: 3 HPA can auto-adjust
# deploy.resources.limits resources.limits CPU/memory per container
# logging: Cluster-level log agg Fluent Bit / Loki DaemonSet
Kompose: Automated Conversion
# Kompose converts docker-compose.yml to Kubernetes manifests.
# Good starting point — always review and tune the output.
# Install kompose
brew install kompose # macOS
curl -L https://github.com/kubernetes/kompose/releases/download/v1.34.0/kompose-linux-amd64 -o kompose
chmod +x kompose && sudo mv kompose /usr/local/bin/
# Convert compose file to K8s manifests (outputs YAML files)
kompose convert -f compose.yml
# Output: web-deployment.yaml, web-service.yaml, db-statefulset.yaml,
# db-service.yaml, postgres-data-persistentvolumeclaim.yaml, ...
# Convert and immediately apply to a cluster
kompose convert -f compose.yml | kubectl apply -f -
# Convert to Helm chart
kompose convert -f compose.yml --chart
# Limitations of kompose output (always review):
# ❌ No resource limits/requests (you must add these)
# ❌ No health probes (add liveness/readiness)
# ❌ Secrets as plaintext ConfigMaps (move to Secrets)
# ❌ Bind mounts become hostPath (replace with PVCs or ConfigMaps)
# ❌ No HPA, no PodDisruptionBudget, no NetworkPolicy
# ✅ Use as a scaffold, not final production manifests
Deployment and Service
# Compose service → K8s Deployment + Service
# Compose:
# services:
# web:
# image: ghcr.io/org/myapp:v1.2.3
# ports:
# - "3000:3000"
# environment:
# NODE_ENV: production
# deploy:
# replicas: 3
# resources:
# limits: { cpus: '0.5', memory: 512M }
# Kubernetes equivalent:
# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Allow 1 extra pod during rollout
maxUnavailable: 0 # Never take a pod down before new one is ready
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: ghcr.io/org/myapp:v1.2.3
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
# web-service.yaml
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 3000
type: ClusterIP # Internal only; use LoadBalancer or Ingress for external
ConfigMaps and Secrets
# Non-secret config → ConfigMap
# Secrets → Secret (base64-encoded, RBAC-controlled)
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
NODE_ENV: production
LOG_LEVEL: info
PORT: "3000"
# Secret (values must be base64-encoded)
apiVersion: v1
kind: Secret
metadata:
name: web-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXM6Ly8... # echo -n 'value' | base64
JWT_SECRET: c2VjcmV0...
# Reference in Deployment
spec:
containers:
- name: web
envFrom:
- configMapRef:
name: web-config # Injects all ConfigMap keys as env vars
- secretRef:
name: web-secrets # Injects all Secret keys as env vars
# Or individual references:
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: web-secrets
key: DATABASE_URL
# Create secrets from command line (avoids putting values in YAML files)
kubectl create secret generic web-secrets \
--from-literal=DATABASE_URL='postgres://user:pass@db:5432/mydb' \
--from-literal=JWT_SECRET='supersecret'
# Or from a .env file
kubectl create secret generic web-secrets --from-env-file=.env.production
# Sealed Secrets (encrypt secrets in git — safe to commit)
# kubeseal --fetch-cert > pub-cert.pem
# kubectl create secret generic web-secrets --dry-run=client -o yaml \
# | kubeseal --cert pub-cert.pem > web-secrets-sealed.yaml
Persistent Storage
# Named volume → PersistentVolumeClaim (PVC)
# StatefulSet for databases (stable network identity + stable storage)
# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce # Single node read/write (block storage: EBS, GCE PD)
# ReadWriteMany — Multiple nodes (NFS, EFS, Azure Files)
# ReadOnlyMany — Multiple nodes read-only
storageClassName: gp3 # AWS EBS gp3; 'standard' for local dev
resources:
requests:
storage: 20Gi
# StatefulSet for Postgres (guarantees stable pod name and PVC)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- secretRef:
name: postgres-secrets
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # Automatically creates PVC per pod
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: gp3
resources:
requests:
storage: 20Gi
Health Probes
# K8s replaces Docker HEALTHCHECK with three probe types:
#
# startupProbe: Is the container done starting? (slow-start apps)
# livenessProbe: Is the container alive? (restart if fails)
# readinessProbe: Is the container ready for traffic? (remove from Service LB if fails)
spec:
containers:
- name: web
image: ghcr.io/org/myapp:v1.2.3
ports:
- containerPort: 3000
# Allow up to 60s for startup (before liveness starts checking)
startupProbe:
httpGet:
path: /health
port: 3000
failureThreshold: 30 # 30 * 2s = 60s max startup time
periodSeconds: 2
# Restart pod if /health returns non-2xx for 3 consecutive checks
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 0 # startupProbe handles the delay
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
# Remove from Service LB if /ready returns non-2xx
# Useful for graceful shutdown: app marks itself not-ready, drain inflight requests
readinessProbe:
httpGet:
path: /ready
port: 3000
periodSeconds: 5
failureThreshold: 2
timeoutSeconds: 3
# Probe types:
# httpGet: HTTP request (most common for web apps)
# tcpSocket: TCP connection (for non-HTTP services like DB)
# exec: Run a command inside the container
# grpc: gRPC health check protocol
Production Patterns
# 1. Horizontal Pod Autoscaler (replaces manual scaling)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Scale up when avg CPU > 70%
# 2. PodDisruptionBudget (safe node drains during upgrades)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2 # Always keep at least 2 pods running during disruptions
selector:
matchLabels:
app: web
# 3. Ingress (replaces nginx port mapping)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: [myapp.example.com]
secretName: myapp-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
# 4. Graceful shutdown (allow in-flight requests to complete)
spec:
terminationGracePeriodSeconds: 60 # 60s to finish inflight requests
containers:
- name: web
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # Wait for LB to remove pod
# 5. Resource quotas per namespace (prevent runaway resource usage)
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-a
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
Series complete. You now have the full Docker skillset from fundamentals to Kubernetes migration. See the Command Reference Cheat Sheet for a quick-access summary of the most important Docker commands across all phases.