10 Common Pitfalls When Migrating Docker‑Compose to Kubernetes
This guide details the ten most frequent issues encountered when converting Docker‑Compose configurations to Kubernetes, explains why direct mappings often fail, and provides concrete examples, correct configurations, validation steps, and best‑practice recommendations to help teams avoid weeks of troubleshooting.
Problem Background
Docker‑Compose is the first‑generation container orchestration tool used by many teams. A typical docker-compose.yml defines services, networks, volumes, and simple health checks. When a system grows and needs multi‑instance scaling, rolling updates, auto‑scaling, load balancing, and robust health checks, Docker‑Compose quickly reaches its limits, prompting a migration to Kubernetes.
Concept Mapping Between Docker‑Compose and Kubernetes
Service (compose) → Service + Deployment: Kubernetes separates "what runs" (Deployment) from "how to access" (Service).
ports: → Service (ClusterIP/NodePort) + Ingress: Port mapping is split into an internal Service and an optional Ingress for external traffic.
volumes: → PersistentVolumeClaim + PersistentVolume + StorageClass: Storage is abstracted to PV/PVC objects.
environment: → ConfigMap + Secret + env: Configuration is decoupled from the image.
depends_on: → initContainers + readinessProbe: Dependency handling is done via init containers and probes.
networks: → CNI network + NetworkPolicy: Kubernetes uses a pluggable CNI model and network policies.
deploy.resources.limits: → resources.requests + limits: Scheduler guarantees (requests) are distinguished from runtime caps (limits).
.env → ConfigMap / Secret / kustomize: Environment files become ConfigMaps or Secrets.
healthcheck: → livenessProbe / readinessProbe / startupProbe: Kubernetes provides three distinct probe types.
restart: → Pod restartPolicy: Restart semantics are expressed in the pod spec.
Ten Common Pitfalls and How to Avoid Them
Pitfall 1 – Using Docker‑Compose Service Names Directly as Kubernetes Service Names
Issue : Teams assume a container name like container-b can be used unchanged as a Service name and accessed via http://container-b:8080.
Reality : In Kubernetes, Pods are accessed through a Service object. The Service DNS resolves to a virtual ClusterIP and load‑balances traffic. Direct DNS lookups may return a ClusterIP, not a single container IP, and long‑lived connections can be pinned to a specific backend when using iptables mode.
Key Points :
Long‑lived connections may stick to a single backend pod (iptables mode).
DNS caching inside the application can break after Service recreation.
# Incorrect Docker‑Compose usage (fails in K8s)
# curl http://container-b:8080
# Correct Kubernetes Service definition
apiVersion: v1
kind: Service
metadata:
name: container-b
spec:
selector:
app: container-b
ports:
- port: 8080
targetPort: 8080Verification :
# Test from any pod
kubectl exec -it mypod -n myns -- curl -s http://container-b:8080
# Verify DNS resolution
kubectl exec -it mypod -n myns -- nslookup container-bPitfall 2 – Assuming depends_on Translates Directly to Kubernetes
Issue : depends_on is treated as a start‑order guarantee.
Reality : Kubernetes does not provide start‑order. Pods start in parallel. Use initContainers for pre‑flight checks or implement retry logic in the application.
# Example initContainer that waits for a DB Service
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ["sh", "-c", "until nslookup db-service; do sleep 2; done"]Key Points :
Init containers run sequentially and block the main container until they succeed.
Application‑level retries are the most robust solution.
Pitfall 3 – Mapping Docker‑Compose volumes Directly to Kubernetes Volumes
Issue : Named volumes are assumed to work the same way.
Reality : Kubernetes uses PV/PVC and StorageClass. AccessMode (RWO vs RWX) matters for multi‑replica workloads.
# PVC for a PostgreSQL database
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 20GiKey Points :
RWO cannot be shared across pods; use RWX or a StatefulSet with per‑pod PVCs.
StatefulSet + volumeClaimTemplates creates a PVC per pod.
Set reclaimPolicy: Retain for stateful data.
Pitfall 4 – Copying .env Files Directly into a ConfigMap
Issue : All key‑value pairs from .env are placed into a ConfigMap, including secrets.
Reality : Sensitive data must go into a Secret. ConfigMaps are for non‑sensitive configuration.
# ConfigMap (non‑sensitive)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DB_HOST: "db-service"
DB_PORT: "5432"
LOG_LEVEL: "info"
# Secret (sensitive)
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque
stringData:
DB_USER: "myapp"
DB_PASS: "supersecret"Key Points :
ConfigMap values are injected as environment variables or mounted as files.
Secrets are base64‑encoded; use kubectl get secret -o yaml to view.
Changes to ConfigMaps/Secrets require a pod restart or rollout.
Pitfall 5 – Treating Docker‑Compose ports as Direct Kubernetes Port Mappings
Issue : "8080:8080" is copied verbatim.
Reality : Port exposure is split into three layers:
Container port declared in the Deployment ( containerPort).
Cluster‑internal Service ( port / targetPort).
External access via NodePort, LoadBalancer, or Ingress.
# Deployment containerPort
containers:
- name: web
image: myapp/web:latest
ports:
- containerPort: 8080
# Service exposing the port inside the cluster
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080
# Ingress for external access
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
spec:
rules:
- host: app.example.com
http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80Key Points :
ClusterIP Service is internal; NodePort/LoadBalancer/Ingress expose it outside. port and targetPort can differ.
Debug missing traffic with kubectl get endpoints and DNS checks.
Pitfall 6 – Ignoring the Distinction Between requests and limits
Issue : Only resources.limits are set, mirroring Compose limits.
Reality : requests are scheduler guarantees; omitting them makes the scheduler treat them as equal to limits, potentially wasting resources.
# Recommended resource spec
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"Key Points :
Set requests based on observed P99 usage.
Set limits to a safe headroom (e.g., 2× P99 CPU).
Validate with kubectl top pod and Prometheus.
Pitfall 7 – Assuming kubectl logs Works Like docker-compose logs
Issue : One command shows logs for all replicas.
Reality : kubectl logs shows a single pod; for multiple replicas use stern or kubectl logs -l app=web --all-containers=true.
Key Points :
Pod restarts lose previous container logs unless --previous is used.
Production requires a centralized logging stack (EFK, Loki, Cloud‑native).
Pitfall 8 – Copying Docker‑Compose Health Checks Directly to Kubernetes Probes
Issue : healthcheck maps 1:1 to livenessProbe.
Reality : Docker‑Compose health checks behave more like readinessProbe. Kubernetes livenessProbe restarts the container on failure.
# Correct probe configuration
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 10
failureThreshold: 2
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 5Key Points :
Use startupProbe for slow‑starting apps.
Separate liveness (restart) from readiness (traffic).
Adjust intervals; Docker‑Compose defaults are not suitable for K8s.
Pitfall 9 – Mapping Each Docker‑Compose Service to a Deployment One‑to‑One
Issue : All services become Deployments.
Reality : Stateful workloads (databases, queues) need StatefulSet for stable network IDs and per‑pod PVCs.
Deployment : Stateless apps, random pod names, shared PVC (if any).
StatefulSet : Stable DNS ( pod-name.service-name), per‑pod PVC, ordered scaling and rolling updates.
# PostgreSQL as a StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: app-secret
key: DB_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: DB_PASS
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: pgdata
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 20GiPitfall 10 – CI/CD Process Not Aligned with Kubernetes
Issue : Continue using docker-compose build && docker-compose up in the pipeline.
Reality : Kubernetes requires image versioning, registry push, and declarative deployment updates.
# Example CI steps
# 1. Build and tag image
TAG=${BRANCH_NAME}-${COMMIT_SHA:0:7}-$(date +%Y%m%d%H%M)
docker build -t registry.example.com/myapp/web:${TAG} .
# 2. Push to registry
docker push registry.example.com/myapp/web:${TAG}
# 3. Update deployment image
kubectl set image deployment/web web=registry.example.com/myapp/web:${TAG} -n production
# 4. Verify rollout
kubectl rollout status deployment/web -n production --timeout=300sKey Points :
Never use :latest in production; use immutable tags.
Configure imagePullSecrets for private registries.
Define rolling‑update strategy ( maxUnavailable, maxSurge, minReadySeconds).
Migration Roadmap
Assessment (1‑2 weeks) : List services, classify as stateless or stateful, inventory volumes, env vars, secrets, and current CI steps.
Phase 1 – Low‑Risk Services (2‑4 weeks) : Migrate front‑end or API services using Deployments and Services. Verify DNS, health, logs.
Phase 2 – Stateful Services (2‑4 weeks) : Migrate databases or queues with StatefulSets, PVCs, and headless Services. Perform data‑migration dry‑runs.
Phase 3 – Full Cut‑over (1 week) : Switch traffic, keep Docker‑Compose as fallback for 1‑2 weeks, then decommission.
Tooling Options
Kompose : Auto‑convert docker‑compose.yml to K8s YAML (good for bootstrapping, not production‑ready).
Kustomize : Overlay management for dev/staging/prod.
Helm : K8s package manager with templating (complex apps, versioned releases).
Skaffold : Local dev ↔︎ K8s workflow (CI/CD integration).
Verification Checklist
Before Migration
Backup all Compose files and volume data.
Record baseline CPU/Memory and latency metrics.
Validate a test K8s cluster with required namespaces, RBAC, and network policies.
After Migration (per service)
Pods are Running with expected replica count.
Service endpoints correctly point to pods.
Ingress routes are reachable from outside.
PVCs are Bound and data persists.
ConfigMaps/Secrets are injected as intended.
Probes do not cause frequent restarts or traffic drops.
Logs are collected by the chosen logging stack.
Metrics appear in Prometheus/Grafana.
Resource usage matches or improves on baseline.
Rollback works via kubectl rollout undo or scaling to zero.
Rollback Strategies
Service‑level rollback (scale down, fix manifest, scale back up):
# Rollback a deployment
kubectl rollout undo deployment/web -n production
# Or force a new rollout
kubectl rollout restart deployment/web -n productionCluster‑wide fallback : Keep the original Docker‑Compose stack running on a separate set of nodes and switch DNS or load‑balancer back to it while fixing K8s issues.
Common Post‑Migration Issues and Diagnosis
Pod CrashLoopBackOff
# Check restart count
kubectl get pod -n ns
# Inspect last state
kubectl describe pod mypod -n ns | grep -A5 "Last State"
# View previous logs
kubectl logs mypod -n ns --previousTypical causes: missing env vars, overly strict livenessProbe, missing imagePullSecrets, hard‑coded hostnames.
Service Communication Timeout
# Verify DNS inside a pod
kubectl exec -it mypod -n ns -- nslookup db-svc
# Check Service endpoints
kubectl get endpoints db-svc -n ns
# Review NetworkPolicy
kubectl get networkpolicy -n nsEnsure Service selector matches pod labels, pods are Ready, and namespaces align.
OOM Kill
# Observe memory usage
kubectl top pod mypod -n ns
# Look for OOM events
kubectl describe pod mypod -n ns | grep -i oom
# Adjust JVM flags for Java apps
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=30.0"ConfigMap/Secret Update Not Reflected
Env‑var injection requires pod restart: kubectl rollout restart deployment/web -n ns.
File‑mount injection updates automatically, but the application must watch the file.
Operational Command Mapping
View status : docker-compose ps → kubectl get pods -n <ns> View logs (single) : docker-compose logs web → kubectl logs <pod> -n <ns> View logs (all replicas) : docker-compose logs -f web → stern -n <ns> web (install stern).
Enter container : docker-compose exec web bash → kubectl exec -it <pod> -n <ns> -- bash Restart service : docker-compose restart web → kubectl rollout restart deployment/web -n <ns> Scale : docker-compose up -d --scale web=5 → kubectl scale deployment/web -n <ns> --replicas=5 Resource usage : docker stats → kubectl top pod -n <ns> Update image : docker-compose pull && docker-compose up -d →
kubectl set image deployment/web web=<new-image> -n <ns>Rollback : manual →
kubectl rollout undo deployment/web -n <ns>Resource Estimation & Cost Considerations
Kubernetes adds system‑component overhead per node (kubelet, kube‑proxy, CNI, logging agent, monitoring agent). Approximate per‑node baseline:
CPU: 220‑570 m
Memory: 440 Mi‑1.2 Gi
Formula for total node resources:
TotalNodeResources = BusinessResources × 1.3 + SystemComponentOverhead × NodeCount + 20% bufferFor a small‑to‑medium workload (~50 pods), a 3‑node cluster of 4 CPU / 8 Gi each typically leaves ~10 CPU / 21 Gi for applications after system overhead.
Final Takeaways
Docker‑Compose manages single‑node multi‑container workloads; Kubernetes manages multi‑node distributed workloads – rethink networking, storage, and scheduling.
Never rely on a one‑click Kompose conversion for production; use the generated YAML as a starting point and refine it.
Migrate stateless services first, then stateful services, and finally the CI/CD pipeline.
Update CI/CD to build, tag, push images, and apply declarative manifests with proper rollout and rollback controls.
After each migration step, verify networking, persistence, configuration, probes, logging, and metrics before moving on.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ops Community
A leading IT operations community where professionals share and grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
