Cloud Native 47 min read

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.

Ops Community
Ops Community
Ops Community
10 Common Pitfalls When Migrating Docker‑Compose to Kubernetes

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: 8080

Verification :

# 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-b

Pitfall 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: 20Gi

Key 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: 80

Key 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: 5

Key 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: 20Gi

Pitfall 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=300s

Key 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 production

Cluster‑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 --previous

Typical 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 ns

Ensure 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 pskubectl get pods -n <ns> View logs (single) : docker-compose logs webkubectl logs <pod> -n <ns> View logs (all replicas) : docker-compose logs -f webstern -n <ns> web (install stern).

Enter container : docker-compose exec web bashkubectl exec -it <pod> -n <ns> -- bash Restart service : docker-compose restart webkubectl rollout restart deployment/web -n <ns> Scale : docker-compose up -d --scale web=5kubectl scale deployment/web -n <ns> --replicas=5 Resource usage : docker statskubectl 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% buffer

For 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MigrationKubernetesDevOpsBest PracticesContainersDocker-Compose
Ops Community
Written by

Ops Community

A leading IT operations community where professionals share and grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.