Pavan Rangani

HomeBlogDocker Compose to Kubernetes: A Practical Migration Playbook

Docker Compose to Kubernetes: A Practical Migration Playbook

By Pavan Rangani · February 7, 2026 · DevOps & Cloud

Docker Compose to Kubernetes: A Practical Migration Playbook

Docker Compose to Kubernetes Migration: A Practical Guide

Your application runs perfectly in Docker Compose on a single server, but now you need high availability, auto-scaling, and zero-downtime deployments. Kubernetes provides these capabilities, but the migration path is full of subtle differences that break applications in production. Therefore, this guide walks through the practical conversion of Docker Compose services to Kubernetes manifests, covering the networking, storage, and configuration changes that trip up most teams. The recurring theme is that Compose assumes a single, stable host, whereas Kubernetes assumes the opposite — pods are cattle, nodes fail, and nothing about an address is permanent.

Why Migrate? Honest Trade-offs

Docker Compose is excellent for single-host deployments, development environments, and small applications. If your app runs on one server and you don’t need auto-scaling or rolling updates, Compose may be the right choice. Moreover, Compose files are simpler to read, faster to deploy, and require less operational knowledge.

Kubernetes makes sense when you need multi-node availability, horizontal auto-scaling, rolling deployments with automatic rollback, or when your organization standardizes on Kubernetes for all workloads. However, Kubernetes adds operational complexity — you need monitoring, RBAC, network policies, and cluster management. Consequently, evaluate whether the benefits justify the complexity for your specific use case. As a rule of thumb, if a single beefy server plus a nightly backup meets your availability target, Kubernetes will mostly add operational overhead rather than value.

# docker-compose.yml — Starting point
version: '3.8'
services:
  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    restart: unless-stopped

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=myapp

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb

volumes:
  pgdata:
Docker Compose to Kubernetes migration architecture
Migrating from Docker Compose requires rethinking networking, storage, and service discovery

Using Kompose for Initial Manifest Generation

Kompose translates Docker Compose files to Kubernetes manifests automatically. It handles the basic conversion — services become Deployments, ports become Services, volumes become PersistentVolumeClaims. Additionally, it generates the boilerplate YAML that would take hours to write manually.

However, Kompose output is a starting point, not production-ready configuration. It doesn’t set resource limits, liveness probes, or security contexts. Furthermore, environment variables with passwords end up as plaintext in the manifests. You must refine every generated file before deploying to production. A useful way to think about Kompose is as a transcription tool, not a translator — it converts the syntax faithfully but understands nothing about your reliability or security requirements.

# Install Kompose
curl -L https://github.com/kubernetes/kompose/releases/latest/download/kompose-linux-amd64 -o kompose
chmod +x kompose && sudo mv kompose /usr/local/bin/

# Convert docker-compose.yml to Kubernetes manifests
kompose convert -f docker-compose.yml -o k8s/

# Generated files:
# k8s/api-deployment.yaml
# k8s/api-service.yaml
# k8s/db-deployment.yaml
# k8s/db-service.yaml
# k8s/cache-deployment.yaml
# k8s/cache-service.yaml
# k8s/pgdata-persistentvolumeclaim.yaml

# Review and customize before applying
kubectl apply -f k8s/ --dry-run=client

Networking: The Biggest Difference

In Docker Compose, services communicate by container name on a shared bridge network. In Kubernetes, services communicate through Service objects with DNS names like db.default.svc.cluster.local. For most cases, the short name db works within the same namespace. Specifically, Kubernetes DNS resolves service names to ClusterIP addresses, which load-balance across pods.

Compose’s depends_on has no Kubernetes equivalent because pods can restart independently. Your application must handle database connection retries gracefully — use exponential backoff in your connection code. In contrast to Compose where services start in order, Kubernetes pods may start before their dependencies are ready. This is the single most common cause of “it worked in Compose but crash-loops in Kubernetes,” and the fix lives in your application, not your YAML.

# Production-ready Kubernetes Deployment with health checks
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  labels:
    app: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: registry.example.com/myapp/api:v1.2.3
          ports:
            - containerPort: 3000
          envFrom:
            - secretRef:
                name: api-secrets
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5

Liveness vs Readiness: Getting Probes Right

The two probes above look similar, yet they do fundamentally different jobs, and conflating them causes outages. A readiness probe controls whether a pod receives traffic; when /ready fails, Kubernetes removes the pod from the Service’s endpoints but leaves it running. A liveness probe controls whether a pod is restarted; when /health fails, the kubelet kills and recreates the container. Consequently, your readiness endpoint should check downstream dependencies (can I reach the database?), while your liveness endpoint should check only the process itself (is my event loop responsive?).

A classic anti-pattern is pointing the liveness probe at a dependency-checking endpoint. Imagine the database briefly hiccups: every API pod’s liveness probe fails simultaneously, Kubernetes restarts all of them at once, and the resulting thundering herd of reconnections finishes off the already-struggling database. Therefore, keep liveness shallow and readiness deep. The example below shows a Node-style readiness handler that fails fast without blocking the liveness path.

// health endpoints — keep them on different code paths
app.get('/health', (_req, res) => {
  // Liveness: is THIS process alive? No external calls.
  res.status(200).json({ status: 'ok' });
});

app.get('/ready', async (_req, res) => {
  // Readiness: can I actually serve a request right now?
  try {
    await Promise.all([
      db.query('SELECT 1'),
      cache.ping(),
    ]);
    res.status(200).json({ status: 'ready' });
  } catch (err) {
    // 503 tells Kubernetes to stop routing traffic, but NOT to restart us
    res.status(503).json({ status: 'degraded', error: err.message });
  }
});
Kubernetes networking and service discovery diagram
Kubernetes service discovery replaces Docker Compose’s simple container name networking

Storage and Secrets Management

Docker Compose volumes are local directories on the host machine. Kubernetes PersistentVolumeClaims (PVCs) abstract storage so pods can move between nodes. For databases, use StatefulSets instead of Deployments — they provide stable network identities and ordered pod management that databases require. The ReadWriteOnce access mode is also worth understanding early: a typical block volume can only attach to one node at a time, so you cannot naively scale a stateful pod to three replicas the way you would a stateless API.

Never put passwords in environment variables within Kubernetes manifests. Use Secrets objects and mount them as environment variables or files. For production, integrate with external secret managers like HashiCorp Vault or AWS Secrets Manager using the External Secrets Operator. As a result, secrets are rotated centrally without redeploying your application. Remember, too, that a Kubernetes Secret is only base64-encoded, not encrypted, unless you explicitly enable encryption at rest in etcd — so treat the raw manifest with the same care as a password file.

# Create secrets from literal values (use External Secrets in production)
apiVersion: v1
kind: Secret
metadata:
  name: api-secrets
type: Opaque
stringData:
  DATABASE_URL: "postgres://user:pass@db:5432/myapp"
  REDIS_URL: "redis://cache:6379"
---
# StatefulSet for PostgreSQL (not Deployment)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
        - name: postgres
          image: postgres:16
          envFrom:
            - secretRef:
                name: db-secrets
          volumeMounts:
            - name: pgdata
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: pgdata
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

Should the Database Live in Kubernetes at All?

Just because you can run PostgreSQL as a StatefulSet does not mean you should. Running a stateful database inside Kubernetes means you now own backups, point-in-time recovery, storage performance tuning, major-version upgrades, and failover orchestration — all of which a managed service like Amazon RDS, Google Cloud SQL, or Azure Database handles for you. For many teams migrating off Compose, the pragmatic path is to move the stateless tiers (the API, workers, and cache) into Kubernetes while pointing them at a managed database over the network.

This hybrid approach sidesteps the riskiest part of the migration. Your DATABASE_URL simply points at a managed endpoint instead of an in-cluster Service, and you lose nothing in resilience — arguably you gain it, because cloud-managed databases offer multi-AZ failover out of the box. Conversely, self-hosting in-cluster makes sense when you have strict data-residency rules, an existing database operator you trust, or a strong reason to keep everything portable across clouds. The honest trade-off is operational burden versus control, and most teams underestimate the burden.

Production Considerations and Deployment Strategy

In production, add Horizontal Pod Autoscalers, Pod Disruption Budgets, and Ingress controllers. Set resource requests and limits on every container — without them, a memory-leaking pod can crash the entire node. Additionally, use namespace isolation to separate environments (staging, production) on the same cluster. A Pod Disruption Budget deserves special mention because it is easy to skip and painful to skip: it tells Kubernetes the minimum number of pods that must stay available during voluntary disruptions like node drains, preventing a routine cluster upgrade from taking your whole API offline at once.

Migrate incrementally. Run your Compose stack and Kubernetes deployment in parallel, routing a percentage of traffic to Kubernetes using a load balancer. Increase the percentage gradually while monitoring error rates and latency. Furthermore, keep your Compose setup as a rollback option until Kubernetes is proven stable under your production load. During this overlap, watch the metrics that actually predict user pain — p99 latency, error rate, and saturation — rather than vanity numbers, and only advance the traffic split when each step holds steady.

Kubernetes production deployment monitoring
Monitor error rates and latency during migration — keep Docker Compose as a rollback option

Related Reading:

Resources:

In conclusion, migrating from Docker Compose to Kubernetes is justified when you need multi-node resilience, auto-scaling, and rolling deployments. Use Kompose for initial manifest generation but customize every file for production — add health probes, resource limits, secrets management, and security contexts. Migrate incrementally and keep Compose as a fallback until your Kubernetes deployment proves itself under real traffic.

← Back to all articles