Van Docker naar Kubernetes: een praktisch migratiepad voor bestaande Compose-projecten
Deze tutorial begeleidt je stap voor stap bij het migreren van een bestaand Docker Compose-project naar Kubernetes (K8s), met een focus op praktische keuzes, valkuilen en realistische commando’s. Je leert niet alleen wat je moet doen, maar vooral waarom je bepaalde Kubernetes-resources inzet en hoe je Compose-concepten vertaalt naar K8s.
Inhoud
- Voor wie is dit?
- Wat verandert er conceptueel?
- Voorbeeld Compose-project
- Voorbereiding: tools en cluster
- Stap 1: Images bouwen en publiceren
- Stap 2: Compose naar Kubernetes mappen (concepten)
- Stap 3: Eerste migratie met Kompose (snelle start)
- Stap 4: Handmatig “productiewaardig” maken
- Stap 5: Deployen, debuggen en verifiëren
- Stap 6: Migratiepad in fases (realistische aanpak)
- Veelvoorkomende valkuilen
- Volgende stap: Helm en GitOps
Voor wie is dit?
- Je hebt een werkend Docker Compose-project (lokaal of op een VM).
- Je wilt naar Kubernetes voor schaalbaarheid, self-healing, betere deployment workflows of platform-standaardisatie.
- Je wilt een migratiepad dat je iteratief kunt uitvoeren zonder alles in één keer te herschrijven.
Wat verandert er conceptueel?
Docker Compose is vooral een developer-friendly orchestrator voor één machine (of een beperkte omgeving). Kubernetes is een cluster orchestrator met andere aannames:
- Compose: “Start deze containers met deze netwerken en volumes.”
- Kubernetes: “Zorg dat deze gewenste toestand (desired state) altijd klopt, over meerdere nodes, met herstartbeleid, rollouts, service discovery, storage abstrahering, en security.”
Belangrijke verschuivingen:
- Container → Pod: In K8s is de kleinste deploybare unit een Pod (één of meerdere containers die samen lifecycle delen).
- docker-compose up → kubectl apply: Je beschrijft resources declaratief en K8s convergeert daarnaar.
- Netwerk: In Compose praat je via servicenaam op één Docker netwerk. In K8s is service discovery standaard via DNS in een namespace.
- Volumes: Compose bind mounts/volumes zijn lokaal; K8s gebruikt PersistentVolumes/Claims en storage classes.
- Configuratie: Compose
.enven environment variables worden in K8s vaak ConfigMaps/Secrets.
Voorbeeld Compose-project
We nemen een typisch project: een webapp (API) + Postgres + Redis.
docker-compose.yml (vereenvoudigd maar realistisch):
version: "3.9"
services:
api:
build: ./api
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://app:app@db:5432/app
- REDIS_URL=redis://cache:6379/0
- LOG_LEVEL=info
depends_on:
- db
- cache
db:
image: postgres:16
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
- POSTGRES_DB=app
volumes:
- dbdata:/var/lib/postgresql/data
cache:
image: redis:7
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
volumes:
dbdata:
redisdata:
Doel in Kubernetes:
apials Deployment + Servicedbals StatefulSet + Service + PersistentVolumeClaimcacheals StatefulSet (of Deployment) + PVC- Config via ConfigMap en Secret
- Externe toegang via Ingress (of LoadBalancer/NodePort)
Voorbereiding: tools en cluster
Lokale tools
Installeer:
kubectldockerofpodman- een lokaal cluster:
kindofminikube - optioneel:
kompose(voor snelle conversie) - optioneel:
helm(later)
Voorbeeld (macOS met Homebrew):
brew install kubectl kind kompose helm
Cluster starten met kind
kind create cluster --name compose-migratie
kubectl cluster-info
kubectl get nodes
Stap 1: Images bouwen en publiceren
In Compose bouw je lokaal met build: ./api. In Kubernetes moeten nodes je image kunnen pullen.
Optie A: Image in kind laden (lokaal)
Bouw de image:
docker build -t my-api:0.1.0 ./api
Laad in kind:
kind load docker-image my-api:0.1.0 --name compose-migratie
Optie B: Push naar een registry (realistischer)
Tag en push (voorbeeld met Docker Hub):
docker tag my-api:0.1.0 jouwdockerhub/my-api:0.1.0
docker push jouwdockerhub/my-api:0.1.0
Als je registry private is, heb je in Kubernetes een imagePullSecret nodig.
Stap 2: Compose naar Kubernetes mappen (concepten)
Hier is een praktische “vertalingstabel”:
| Compose | Kubernetes | Opmerking |
|---|---|---|
| service | Deployment/StatefulSet + Service | Stateless → Deployment, stateful → StatefulSet |
| ports: “8080:8080” | Service + (Ingress/NodePort/LB) | K8s expose is losgekoppeld |
| environment | ConfigMap/Secret + envFrom/env | Secrets voor wachtwoorden/keys |
| depends_on | Readiness probes + initContainers (soms) | K8s startvolgorde is niet gegarandeerd |
| volumes | PVC + StorageClass | Bind mounts zijn anti-pattern in clusters |
| restart: always | default behavior | K8s herstart pods automatisch |
| networks | K8s cluster netwerk + namespaces | Service discovery via DNS |
Belangrijk: depends_on in Compose is vaak een schijnveiligheid. In K8s moet je app robuust zijn: retries, timeouts, en readiness checks.
Stap 3: Eerste migratie met Kompose (snelle start)
Kompose kan Compose omzetten naar Kubernetes manifests. Dit is zelden “productieklaar”, maar het is nuttig om snel een baseline te krijgen.
Conversie
In de directory met docker-compose.yml:
kompose convert
ls -la
Je krijgt meestal bestanden zoals:
api-deployment.yamlapi-service.yamldb-deployment.yaml(let op: vaak Deployment i.p.v. StatefulSet)db-service.yaml- PVC’s afhankelijk van volumes
Deployen
kubectl apply -f .
kubectl get pods
kubectl get svc
Waarom dit slechts een start is
Kompose:
- kiest vaak Deployment voor databases (niet ideaal)
- maakt soms onhandige labels/selectors
- gebruikt niet altijd Secrets/ConfigMaps op een veilige manier
- voegt geen probes, resource limits, of Ingress toe
Gebruik dit dus om te leren en te itereren, niet als eindresultaat.
Stap 4: Handmatig “productiewaardig” maken
Hier bouwen we een nette set manifests. Je kunt dit in één map zetten, bijvoorbeeld k8s/.
Namespaces
Een namespace helpt bij isolatie en opruimen.
kubectl create namespace app
kubectl config set-context --current --namespace=app
kubectl get ns
Deployments en Services
API Deployment
We scheiden configuratie (ConfigMap/Secret) van de Deployment.
Maak k8s/api-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels:
app: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: my-api:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secrets
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
Waarom dit belangrijk is:
- replicas: Compose draait meestal één instance; K8s maakt horizontale scaling triviaal.
- readinessProbe: voorkomt dat verkeer naar een pod gaat die nog niet klaar is.
- livenessProbe: herstart een pod die “hangt”.
- resources: zonder requests/limits kan één pod een node “opeten” of onvoorspelbaar schedulen.
API Service (intern)
Maak k8s/api-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- name: http
port: 80
targetPort: 8080
type: ClusterIP
In Compose deed je 8080:8080 om host-toegang te krijgen. In K8s is dit intern; extern regelen we later via Ingress of port-forward.
ConfigMaps en Secrets
In Compose zet je vaak alles in environment. In K8s:
- ConfigMap: niet-gevoelige config (log level, feature flags, hostnames)
- Secret: wachtwoorden, tokens, private keys (base64-encoded; voor echte security: encryptie at rest + externe secret stores)
Maak k8s/api-config.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
LOG_LEVEL: "info"
REDIS_URL: "redis://cache:6379/0"
DATABASE_HOST: "db"
DATABASE_NAME: "app"
DATABASE_PORT: "5432"
Maak k8s/api-secrets.yaml:
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
DATABASE_USER: "app"
DATABASE_PASSWORD: "app"
En pas je app aan zodat DATABASE_URL samengesteld kan worden, of maak DATABASE_URL in de ConfigMap/Secret. Bijvoorbeeld:
stringData:
DATABASE_URL: "postgres://app:app@db:5432/app"
Praktisch beheer via CLI:
kubectl apply -f k8s/api-config.yaml
kubectl apply -f k8s/api-secrets.yaml
kubectl describe configmap api-config
kubectl describe secret api-secrets
Persistent storage
Compose volumes dbdata en redisdata zijn lokaal. In Kubernetes wil je een PersistentVolumeClaim (PVC) die door een StorageClass wordt ingevuld.
Check beschikbare storage classes:
kubectl get storageclass
Postgres als StatefulSet
Voor databases is StatefulSet meestal beter dan Deployment:
- stabiele network identity (
db-0,db-1, …) - stabiele storage per replica
- gecontroleerde rollout/ordening
Maak k8s/db-statefulset.yaml:
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
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
value: app
volumeMounts:
- name: dbdata
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
initialDelaySeconds: 15
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: dbdata
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Maak k8s/db-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: db
spec:
selector:
app: db
ports:
- name: postgres
port: 5432
targetPort: 5432
clusterIP: None
Let op: clusterIP: None maakt dit een headless service, nuttig voor StatefulSets. Voor 1 replica werkt het ook prima.
Maak k8s/db-secrets.yaml:
apiVersion: v1
kind: Secret
metadata:
name: db-secrets
type: Opaque
stringData:
POSTGRES_USER: "app"
POSTGRES_PASSWORD: "app"
Deploy:
kubectl apply -f k8s/db-secrets.yaml
kubectl apply -f k8s/db-service.yaml
kubectl apply -f k8s/db-statefulset.yaml
kubectl get pods -l app=db
kubectl get pvc
Redis met persistence
Redis kan ook stateful zijn als je AOF/RDB gebruikt.
Maak k8s/cache-statefulset.yaml:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cache
spec:
serviceName: cache
replicas: 1
selector:
matchLabels:
app: cache
template:
metadata:
labels:
app: cache
spec:
containers:
- name: redis
image: redis:7
args: ["redis-server", "--appendonly", "yes"]
ports:
- containerPort: 6379
volumeMounts:
- name: redisdata
mountPath: /data
readinessProbe:
exec:
command: ["sh", "-c", "redis-cli ping | grep PONG"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["sh", "-c", "redis-cli ping | grep PONG"]
initialDelaySeconds: 15
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: redisdata
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 2Gi
Maak k8s/cache-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: cache
spec:
selector:
app: cache
ports:
- name: redis
port: 6379
targetPort: 6379
clusterIP: None
Deploy:
kubectl apply -f k8s/cache-service.yaml
kubectl apply -f k8s/cache-statefulset.yaml
kubectl get pods -l app=cache
kubectl get pvc
Health checks: readiness en liveness
In Compose heb je soms healthcheck:. In K8s zijn probes cruciaal:
- readinessProbe: “mag ik verkeer ontvangen?”
- livenessProbe: “ben ik nog gezond of moet ik herstarten?”
- startupProbe (optioneel): “ik heb lang nodig om op te starten, wacht met liveness”
Voor API’s is een HTTP endpoint ideaal. Voor databases vaak exec (zoals pg_isready).
Test je endpoints lokaal of in cluster:
kubectl port-forward deploy/api 8080:8080
curl -i http://localhost:8080/health/ready
curl -i http://localhost:8080/health/live
Resources en autoscaling
Zonder resource-instellingen kan autoscaling niet goed werken en is scheduling onvoorspelbaar.
HPA (Horizontal Pod Autoscaler)
Voorbeeld: schaal API tussen 2 en 6 replicas op CPU.
kubectl autoscale deployment api --cpu-percent=70 --min=2 --max=6
kubectl get hpa
Let op: dit vereist meestal een metrics server. In kind/minikube moet je die soms apart installeren.
Check:
kubectl top pods
kubectl top nodes
Netwerk en Ingress
In Compose expose je met ports. In K8s heb je opties:
kubectl port-forward(dev)- Service
NodePort(simpel, maar grof) - Service
LoadBalancer(cloud) - Ingress (meest gangbaar voor HTTP)
Ingress controller installeren (voorbeeld: NGINX)
In kind kan dit, maar vereist extra stappen. Voor een generieke aanpak (werkt vaak op veel clusters):
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.2/deploy/static/provider/cloud/deploy.yaml
kubectl -n ingress-nginx rollout status deploy/ingress-nginx-controller
Maak k8s/api-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
spec:
ingressClassName: nginx
rules:
- host: api.localtest.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
Deploy:
kubectl apply -f k8s/api-ingress.yaml
kubectl get ingress
Test (afhankelijk van je omgeving):
curl -H "Host: api.localtest.me" http://127.0.0.1/
Als Ingress in jouw setup niet direct werkt, gebruik tijdelijk:
kubectl port-forward svc/api 8080:80
curl -i http://localhost:8080/
Stap 5: Deployen, debuggen en verifiëren
Alles toepassen
Als je manifests in k8s/ staan:
kubectl apply -f k8s/
kubectl get all
kubectl get pods
kubectl get pvc
Logs en events
kubectl logs deploy/api
kubectl logs -l app=db
kubectl describe pod <podnaam>
kubectl get events --sort-by=.metadata.creationTimestamp
In een pod kijken (debug)
kubectl exec -it deploy/api -- sh
# binnen de container:
printenv | sort
DNS/service discovery testen:
kubectl exec -it deploy/api -- sh -c "getent hosts db; getent hosts cache"
Database connectiviteit (als je image tools heeft; anders gebruik een tijdelijke debug pod):
kubectl run psql-client --rm -it --image=postgres:16 -- bash
# binnen:
psql -h db -U app -d app
Stap 6: Migratiepad in fases (realistische aanpak)
Een “big bang” migratie is riskant. Dit pad werkt in de praktijk:
Fase 0: Compose opschonen
- Zorg dat services stateless zijn waar mogelijk.
- Maak health endpoints.
- Verplaats config naar env vars.
- Zorg voor retries op DB/Redis connecties.
Fase 1: Kubernetes draaien zonder Ingress
- Deploy alles intern.
- Gebruik
kubectl port-forwardvoor toegang. - Focus op stabiliteit: probes, resources, storage.
Fase 2: Externe toegang
- Voeg Ingress toe.
- Voeg TLS toe (bijv. cert-manager) als je klaar bent.
Fase 3: CI/CD
- Bouw images in CI.
- Push naar registry.
kubectl applyof Helm upgrade in pipeline.
Fase 4: Observability en policies
- Central logging (bijv. Loki/ELK)
- Metrics (Prometheus/Grafana)
- NetworkPolicies (beperk verkeer)
- PodSecurity (security context, non-root)
Veelvoorkomende valkuilen
-
“depends_on” missen
- Oplossing: readiness probes + retries/backoff in de app. Kubernetes garandeert geen startvolgorde.
-
Database als Deployment
- Oplossing: StatefulSet + PVC. Of beter: managed database buiten het cluster.
-
Bind mounts willen gebruiken
- In clusters is lokale disk per node niet betrouwbaar. Gebruik PVC’s of object storage.
-
Geen resource limits
- Kan leiden tot instabiliteit en noisy neighbor-problemen.
-
Secrets in Git
- Gebruik externe secret managers of minstens sealed-secrets/sops. In deze tutorial staan secrets in plain manifests voor leerdoeleinden.
-
Te snel naar “microservices” denken
- Migreren naar Kubernetes betekent niet automatisch herarchitectureren. Eerst “lift and shift” met minimale wijzigingen, daarna optimaliseren.
Volgende stap: Helm en GitOps
Als je manifests groeien, wordt templating en release management belangrijk.
- Helm: package manager voor Kubernetes, maakt parametriseerbare charts.
- Kustomize: overlays per omgeving (dev/staging/prod) zonder templates.
- GitOps (Argo CD/Flux): cluster volgt automatisch wat in Git staat.
Een logische volgende stap is:
- basis manifests → Kustomize overlays → Helm (als je herbruikbare charts wilt) → GitOps voor continue reconciliatie.
Samenvatting
Je hebt nu een praktisch migratiepad:
- Compose-services vertaald naar Deployments/StatefulSets + Services
- Config gescheiden in ConfigMaps en Secrets
- Storage gemigreerd naar PVC’s
- Probes toegevoegd voor betrouwbaarheid
- Externe toegang geregeld via Ingress (of tijdelijk port-forward)
- Debug- en verificatiecommando’s om problemen op te lossen
Als je wilt, kun je jouw eigen docker-compose.yml delen (geanonimiseerd). Dan kan ik een concrete mapping voorstellen (welke services Deployment vs StatefulSet, welke config in ConfigMap/Secret, en een minimale set manifests om te starten).