How to Migrate from Docker Compose to Kubernetes
How do I migrate from Docker Compose to Kubernetes?
TL;DR
- Bottom line: Use
kompose convert(v1.38, Jan 2026) to auto-generate Kubernetes manifests, then refine with health checks, resource limits, secrets, and NetworkPolicies — expect 70-80% auto-conversion. - Key tool/command:
kompose convert -f docker-compose.yml - Watch out for: Kompose skips
build:directives — images must be pre-built and pushed to a registry. No readiness probes or resource limits are generated. Ingress-NGINX retired on 2026-03-24 — use Gateway API or an alternative controller (F5 NGINX Ingress, HAProxy, Traefik) for new clusters. [src9] - Works with: Docker Compose v2/v3, Kubernetes 1.25-1.36, Kompose 1.34-1.38, Helm 3.x, Kustomize, Move2Kube 0.3.15+.
Constraints
- Kompose skips
build:directives — all images must be pre-built and pushed to a container registry before conversion [src1, src2] - Kubernetes 1.25+ removed PodSecurityPolicy — use Pod Security Admission (PSA) namespace labels instead [src7]
- Never deploy without
resources.requestsandresources.limits— a single pod without limits can consume all node resources [src3] - Kubernetes Secrets are base64-encoded, not encrypted at rest by default — enable encryption at rest or use an external secret manager (Vault, Sealed Secrets, AWS Secrets Manager) [src3]
- Ingress-NGINX retired 2026-03-24 — no further releases, bugfixes, or security patches. Migrate to Gateway API or an alternative (F5 NGINX Ingress Controller, HAProxy, Traefik) [src9]
- Gateway API (v1.4+) is the recommended replacement for legacy Ingress on new deployments [src8]
gitRepovolume plugin was permanently removed in Kubernetes 1.36 (2026-04-22) — use initContainers cloning into emptyDir instead [src7]- Docker Compose
depends_onhas no Kubernetes equivalent — use init containers, readiness probes, or application-level connection retry logic [src6]
Quick Reference
| Docker Compose | Kubernetes Equivalent | Notes |
|---|---|---|
services: |
Deployment + Service | Each compose service becomes one Deployment + one Service [src1] |
image: |
containers[].image |
Direct mapping in pod spec [src1] |
build: |
(skipped by Kompose) | Pre-build and push to registry (Docker Hub, ECR, GCR, GHCR) [src6] |
ports: |
Service (ClusterIP/LoadBalancer) | Or use Gateway API HTTPRoute [src1, src8] |
volumes: (named) |
PersistentVolumeClaim | Kompose creates PVCs automatically [src1, src2] |
volumes: (bind mount) |
ConfigMap or hostPath | ConfigMap for config files; avoid hostPath in production [src6] |
environment: |
env: or ConfigMap |
Sensitive values must use K8s Secret [src3] |
env_file: |
ConfigMap from file | kubectl create configmap --from-env-file=.env [src1] |
depends_on: |
(no equivalent) | Use init containers or readiness probes [src6] |
restart: always |
restartPolicy: Always |
Default in Deployments [src7] |
networks: |
K8s networking (flat) + NetworkPolicy | All pods reachable by service name; use NetworkPolicy to restrict [src7] |
deploy.replicas: |
spec.replicas: |
Direct mapping; add HPA for auto-scaling [src2] |
deploy.resources: |
resources.requests/limits |
CPU and memory [src3] |
healthcheck: |
liveness/readiness/startupProbe | Add manually — Kompose may not convert [src6] |
secrets: |
Secret | kubectl create secret generic or Sealed Secrets for GitOps [src3] |
Decision Tree
START
├── Is this for local development only?
│ ├── YES → Keep Docker Compose (K8s is overkill) [src3]
│ └── NO → Continue ↓
├── How many services?
│ ├── 1-3 → Kompose convert [src1, src2]
│ ├── 4-10 → Kompose + Helm charts or Kustomize [src4]
│ └── 10+ → Full Helm chart architecture with subcharts, or Move2Kube [src4, src5]
├── Do you need auto-scaling?
│ ├── YES → Kubernetes HPA [src7]
│ └── NO → Fixed replicas
├── Where will K8s run?
│ ├── Local → Minikube, kind, or k3d [src1]
│ ├── Cloud → EKS, GKE, or AKS [src3]
│ └── Self-hosted → kubeadm or k3s
├── Environment-specific configs?
│ ├── YES (simple overlays) → Kustomize [src3]
│ ├── YES (complex templating) → Helm values.yaml per env [src4]
│ └── NO → Plain manifests
├── Need advanced source analysis?
│ ├── YES → Move2Kube (plan → transform) [src5]
│ └── NO → Kompose is sufficient
└── DEFAULT → kompose convert → refine → Helm/Kustomize → deploy
Step-by-Step Guide
1. Install Kompose and convert
# Install Kompose (v1.38 as of Feb 2026)
# macOS:
brew install kompose
# Linux:
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
kompose convert -f docker-compose.yml
# Output: deployment.yaml, service.yaml, pvc.yaml per service
# Alternative: convert directly to Helm chart
kompose convert -f docker-compose.yml --chart
Verify: ls *.yaml — one Deployment + Service per compose service. [src1, src2]
2. Push images to registry
Kompose skips build: directives — all images must be in a registry. [src6]
# Build and push each service image
docker build -t myregistry/web:1.0 ./web && docker push myregistry/web:1.0
docker build -t myregistry/api:1.0 ./api && docker push myregistry/api:1.0
# Update generated YAML image references
sed -i 's|web:latest|myregistry/web:1.0|g' web-deployment.yaml
sed -i 's|api:latest|myregistry/api:1.0|g' api-deployment.yaml
3. Add health checks and resource limits
Kompose output lacks production essentials — add manually. [src3, src6]
# Add to each container spec in deployment.yaml
containers:
- name: web
image: myregistry/web:1.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
4. Convert env vars to Secrets/ConfigMaps
# Non-sensitive → ConfigMap
kubectl create configmap app-config \
--from-literal=NODE_ENV=production \
--from-literal=LOG_LEVEL=info
# Sensitive → Secret
kubectl create secret generic app-secrets \
--from-literal=DATABASE_URL='postgres://user:pass@db:5432/myapp' \
--from-literal=API_KEY='sk-xxx'
# For GitOps: use Sealed Secrets to safely commit encrypted secrets
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# Reference in Deployment
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
5. Set up Gateway API or Ingress
Gateway API is the recommended approach for new clusters (K8s 1.31+). Ingress-NGINX retired on 2026-03-24 — if you must stay on Ingress, pick an actively maintained controller (F5 NGINX Ingress, HAProxy, Traefik). [src7, src8, src9]
# Option A: Gateway API (recommended for new deployments)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-route
spec:
parentRefs:
- name: my-gateway
hostnames: ["myapp.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: web
port: 8080
# Option B: Legacy Ingress (still widely supported)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts: [myapp.com]
secretName: myapp-tls
rules:
- host: myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 8080
6. Add NetworkPolicies
Compose networks: isolation does not carry over — by default all K8s pods can communicate. [src7]
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-web-only
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- podSelector:
matchLabels:
app: web
ports:
- port: 3000
7. Package as Helm chart or Kustomize
# Option A: Helm chart
helm create myapp
# Replace templates/ with refined manifests
# Parameterize values in values.yaml
helm install myapp ./myapp -f values-production.yaml
helm upgrade myapp ./myapp -f values-production.yaml
# Option B: Kustomize overlays (built into kubectl)
kubectl apply -k overlays/production/
Code Examples
Python: Programmatic Kompose conversion with validation
Full script: python-programmatic-kompose-conversion-with-valida.py (33 lines)
# Input: docker-compose.yml path
# Output: Validated Kubernetes manifests
import subprocess
import yaml
from pathlib import Path
def convert_and_validate(compose_file: str, output_dir: str = "./k8s") -> list[str]:
"""Convert docker-compose.yml to K8s manifests and validate."""
Path(output_dir).mkdir(exist_ok=True)
result = subprocess.run(
["kompose", "convert", "-f", compose_file, "--out", output_dir],
capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError(f"Kompose failed: {result.stderr}")
manifests = list(Path(output_dir).glob("*.yaml"))
issues = []
for f in manifests:
doc = yaml.safe_load(f.read_text())
kind = doc.get("kind", "Unknown")
if kind == "Deployment":
containers = doc["spec"]["template"]["spec"]["containers"]
for c in containers:
if "livenessProbe" not in c:
issues.append(f"{f.name}: {c['name']} missing livenessProbe")
if "resources" not in c:
issues.append(f"{f.name}: {c['name']} missing resource limits")
if issues:
print("Production readiness issues:")
for i in issues:
print(f" - {i}")
return [str(f) for f in manifests]
Bash: End-to-end migration script
Full script: bash-end-to-end-migration-script.sh (27 lines)
#!/bin/bash
# Input: docker-compose.yml
# Output: Deployed Kubernetes application
set -euo pipefail
COMPOSE_FILE="${1:-docker-compose.yml}"
NAMESPACE="${2:-default}"
OUTPUT_DIR="./k8s-manifests"
echo "=== Docker Compose to Kubernetes Migration ==="
mkdir -p "$OUTPUT_DIR"
kompose convert -f "$COMPOSE_FILE" --out "$OUTPUT_DIR"
for f in "$OUTPUT_DIR"/*.yaml; do
kubectl apply --dry-run=client -f "$f" 2>&1 | grep -v "configured" || true
done
kubectl apply -f "$OUTPUT_DIR/" -n "$NAMESPACE"
for deployment in $(kubectl get deployments -n "$NAMESPACE" -o name); do
echo "Waiting for $deployment..."
kubectl rollout status "$deployment" -n "$NAMESPACE" --timeout=120s
done
echo "=== Migration complete ==="
kubectl get all -n "$NAMESPACE"
Alternative: Move2Kube for complex migrations
For larger projects with multiple Dockerfiles and source code, Move2Kube (CNCF Konveyor) provides a more comprehensive migration path. [src5]
# Install Move2Kube (v0.3.15+)
curl -L https://github.com/konveyor/move2kube/releases/latest/download/move2kube-linux-amd64 -o move2kube
chmod +x move2kube && sudo mv move2kube /usr/local/bin/
# Step 1: Plan — analyzes Compose + Dockerfiles + source
move2kube plan -s ./my-compose-project
# Step 2: Transform — interactive Q&A, generates K8s manifests + Helm + CI/CD
move2kube transform -s ./my-compose-project
# Output: deploy/yamls/, deploy/cicd/, deploy/scripts/
Anti-Patterns
Wrong: hostPath volumes in production
# BAD — hostPath ties pods to specific nodes and breaks scaling
volumes:
- name: data
hostPath:
path: /data/myapp
Correct: PersistentVolumeClaims
# GOOD — PVCs are portable and work with cloud storage [src1]
volumes:
- name: data
persistentVolumeClaim:
claimName: myapp-data
Wrong: Secrets in ConfigMaps or env vars in plain text
# BAD — secrets visible in kubectl describe
env:
- name: DATABASE_PASSWORD
value: "my-secret-password"
Correct: Kubernetes Secrets or external secret managers
# GOOD — secrets stored securely [src3]
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
Wrong: Relying on depends_on for service ordering
# BAD — Kubernetes has no depends_on
# Compose: depends_on: [db, redis]
# This causes startup crashes when dependencies aren't ready
Correct: Init containers and readiness probes
# GOOD — init container waits for DB before app starts [src6]
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db-service 5432; do sleep 2; done']
Wrong: Deploying without resource limits
# BAD — pod can consume all node resources and trigger OOM kills
containers:
- name: api
image: myregistry/api:1.0
# No resources block = unlimited
Correct: Always set resource requests and limits
# GOOD — bounded resource usage, enables HPA [src3]
containers:
- name: api
image: myregistry/api:1.0
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
Common Pitfalls
- Kompose skips
build:directives: Images must be pre-built and in a registry. Fix: build, tag, push before conversion. [src6] - No readiness probes generated: Services receive traffic before ready. Fix: add
readinessProbe,livenessProbe, andstartupProbeto every container. [src3, src6] depends_onnot converted: K8s doesn't guarantee startup order. Fix: init containers or connection retry logic. [src6]- Bind mounts don't work in K8s: Local paths don't exist on cluster nodes. Fix: ConfigMaps for config, PVCs for data, Secrets for credentials. [src1]
- Default resource limits too permissive: Without limits, one pod can starve the node. Fix: always set
resources.requestsandresources.limits. [src3] - Missing NetworkPolicies: Compose
networks:doesn't create K8s NetworkPolicies. Fix: implement NetworkPolicies to restrict pod-to-pod traffic. [src7] - PodSecurityPolicy removed in K8s 1.25: Outdated guides reference PSP. Fix: use Pod Security Admission (PSA) namespace labels. [src7]
- Secrets not encrypted at rest: K8s Secrets are only base64-encoded by default. Fix: enable etcd encryption at rest, or use Sealed Secrets / external secret managers. [src3]
- Choosing Ingress-NGINX in 2026: The project was retired on 2026-03-24 — no more security patches. Fix: pick Gateway API for new clusters, or migrate to F5 NGINX Ingress Controller, HAProxy, or Traefik for an Ingress-compatible drop-in. [src9]
- Using
gitRepovolumes: Permanently removed in Kubernetes 1.36 (deprecated since 1.11). Fix: use an initContainer that clones the repo into anemptyDirvolume, then mount that volume in the main container. [src7]
Diagnostic Commands
# Validate compose file before conversion
docker compose config -f docker-compose.yml
# Convert and see what Kompose generates (stdout preview)
kompose convert -f docker-compose.yml --stdout
# Dry-run deployment (validates manifests without applying)
kubectl apply --dry-run=client -f k8s-manifests/
# Check pod status after deployment
kubectl get pods -o wide
kubectl describe pod <pod-name>
kubectl logs <pod-name> --previous # logs from crashed container
# Verify services and endpoints
kubectl get svc
kubectl get endpoints
# Test service connectivity
kubectl run test --rm -it --image=busybox:1.36 -- wget -qO- http://web-service:8080/health
# Check Gateway API routes
kubectl get httproutes
kubectl describe httproute app-route
# Verify NetworkPolicies
kubectl get networkpolicies
kubectl describe networkpolicy api-allow-web-only
# Check resource usage vs limits
kubectl top pods
kubectl top nodes
Version History & Compatibility
| Tool | Version | Status | Key Changes |
|---|---|---|---|
| Kompose | 1.38 (Jan 2026) | Current | Compose Spec v3 full support, Helm chart output, ARM64 [src2] |
| Kubernetes | 1.36 "Haru" (Apr 2026) | Current | User Namespaces GA, Mutating Admission Policies GA, OCI VolumeSource GA, SELinux mount labels GA, gitRepo volume removed [src7] |
| Kubernetes | 1.35 (Dec 2025) | Supported | Timbernetes release, continued Gateway API improvements [src7] |
| Kubernetes | 1.33 (Apr 2025) | Supported | Sidecar containers GA, native init container restartPolicy:Always [src7] |
| Helm | 3.16+ | Current | OCI registry support, improved schema validation [src4] |
| Docker Compose | v2 (Go) | Current | docker compose (space) replaces docker-compose (hyphen) |
| Move2Kube | 0.3.15 (Mar 2025) | Current | Enhanced Compose parsing, Helm + Kustomize output [src5] |
| Gateway API | v1.4 (Nov 2025) | Current | External auth filter, BackendTLSPolicy stable [src8] |
| Ingress-NGINX | EOL 2026-03-24 | Retired | No more releases, bugfixes, or security patches — migrate to Gateway API or alternative controller [src9] |
When to Use / When Not to Use
| Migrate When | Don't Migrate When | Use Instead |
|---|---|---|
| Production workloads need scaling | Local development only | Keep Docker Compose |
| Multiple environments (staging, prod) | Team < 3 engineers, single-server | Docker Compose + systemd |
| Need rolling updates, self-healing | Budget doesn't cover K8s ops | Railway, Fly.io, or Render |
| Cloud-managed K8s available (EKS/GKE/AKS) | Single stateless container | Cloud Run, App Runner, Fargate |
| Need service mesh or mTLS | No compliance/multi-tenancy needs | Docker Compose + Traefik |
| GitOps workflow required | Prototype or proof-of-concept | Docker Compose |
Important Caveats
- Docker Compose is still best for local dev — many teams use hybrid (Compose locally + K8s in production). Tools like Tilt and Skaffold bridge the gap.
- Kompose is a starting point, not production-ready output. Always review and refine — especially probes, resource limits, secrets, and NetworkPolicies.
- Budget 2-4 weeks for team K8s training before production migration. Consider starting with a managed service (EKS, GKE, AKS) to reduce ops burden.
- Storage classes vary by cloud provider — PVC behavior and default StorageClasses differ between EKS (gp3), GKE (pd-standard), and AKS (managed-premium).
- Kubernetes 1.33+ graduated sidecar containers to stable — use
restartPolicy: Alwayson init containers for proper sidecar lifecycle management. - Move2Kube (CNCF Konveyor) is a more comprehensive alternative to Kompose for projects with complex source analysis needs — it generates Helm charts, CI/CD pipelines, and Kustomize overlays in one pass.
- Ingress-NGINX is end-of-life as of 2026-03-24. If your old migration notes recommend
ingressClassName: nginx, treat them as outdated. New clusters should default to Gateway API + HTTPRoute; existing clusters running ingress-nginx should migrate to F5 NGINX Ingress Controller, HAProxy, or Traefik to keep receiving security patches. [src9] - Kubernetes 1.36 (Apr 2026) graduated User Namespaces to GA — container root maps to a non-privileged user on the host, materially improving container escape isolation. Worth enabling for new deployments. [src7]