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.

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.