Migreren van Docker Compose naar Kubernetes: een stap-voor-stap-tutorial

Kompose zet je Docker Compose-bestand in één commando om naar Kubernetes-manifesten. De output ziet er compleet uit, maar mist resource limits, health probes en fatsoenlijke secrets-afhandeling. Deze tutorial doorloopt het volledige migratiepad: conversie met Kompose, en daarna elk manifest hardenen tot het productiewaardig is.

Inhoudsopgave

Wat je leert

Aan het eind van deze tutorial heb je een Docker Compose-bestand omgezet naar Kubernetes-manifesten en die versterkt met resource limits, health probes, fatsoenlijke secrets en de juiste deployment strategy. Je begrijpt dan waarom Kompose-output een startpunt is, geen eindproduct.

Startpunt

Deze tutorial gaat ervan uit dat je het volgende hebt:

  • Een werkend docker-compose.yml in Compose V3-formaat, met minimaal twee services (een webapplicatie en een database)
  • Een Kubernetes-cluster met kubectl geconfigureerd en verbonden (Minikube, kind, of een managed cloudcluster)
  • kubectl versie 1.28 of nieuwer
  • Basiskennis van YAML en de commandline
  • Container-images die al gebuild en gepusht zijn naar een registry (Kompose bouwt standaard geen images voor je)

De voorbeelden gebruiken een WordPress + MySQL-stack omdat die alle migratie-uitdagingen afdekt: volumes, secrets, dependency-volgorde en health checks. Dezelfde principes gelden voor elk multi-service Compose-bestand.

Hoe Docker Compose mapt op Kubernetes

Snap eerst wat wel en niet vertaalt voordat je een tool aanraakt. Deze tabel voorkomt de meest voorkomende verrassingen.

Docker Compose-concept Kubernetes-equivalent Mapt netjes?
services: Deployment + Service per service Ja
image: Container image in pod spec Ja
ports: Service met port en targetPort Ja
environment: env: in container spec (plaintext) Technisch wel, maar een beveiligingsprobleem
volumes: (named) PersistentVolumeClaim Gedeeltelijk: vereist StorageClass, access mode, grootte
depends_on: Niks. Gebruik init containers Nee
networks: Genegeerd. Kubernetes gebruikt flat networking Nee
build: Genegeerd. Bouw je images van tevoren Nee
restart: always restartPolicy: Always (standaard) Ja

Bron: Kompose-conversiematrix

De velden die niet netjes mappen zijn precies de velden die productie-incidenten veroorzaken als je ze niet aanpakt.

Stap 1: audit je docker-compose.yml

Loop voor de conversie door je Compose-bestand en categoriseer elke configuratiewaarde.

Neem deze typische WordPress-stack:

# docker-compose.yml (Compose V3)
services:
  wordpress:
    image: wordpress:6.7-apache
    ports:
      - "80:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: verander-me-in-productie  # gevoelig
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp-content:/var/www/html/wp-content
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root-verander-me    # gevoelig
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: verander-me-in-productie  # gevoelig
    volumes:
      - db-data:/var/lib/mysql

volumes:
  wp-content:
  db-data:

Classificeer elk element:

  • Gevoelige environment variables (WORDPRESS_DB_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD): worden Kubernetes Secrets
  • Niet-gevoelige environment variables (WORDPRESS_DB_HOST, WORDPRESS_DB_NAME): worden een ConfigMap
  • Named volumes (wp-content, db-data): worden PersistentVolumeClaims met een expliciete StorageClass en grootte
  • depends_on: vereist een init container op de WordPress Deployment
  • networks (hier niet aanwezig, maar vaak wel): worden door Kompose genegeerd

Checkpoint: je kunt elke gevoelige waarde, elk volume en elke dependency in je Compose-bestand benoemen voordat je verder gaat.

Stap 2: converteer met Kompose

Kompose is een officieel Kubernetes-incubatorproject dat Docker Compose-bestanden omzet naar Kubernetes-manifesten.

Installeer het:

# Linux (Kompose v1.34.0)
curl -L https://github.com/kubernetes/kompose/releases/download/v1.34.0/kompose-linux-amd64 -o kompose
chmod +x kompose
sudo mv ./kompose /usr/local/bin/kompose

# macOS
brew install kompose

Draai de conversie:

kompose convert -f docker-compose.yml

Je krijgt waarschuwingen als deze:

WARN Unsupported depends_on key - ignoring
WARN Unsupported networks key - ignoring

Die waarschuwingen betekenen dat die velden stilletjes uit de output zijn gelaten. Kompose is niet stuk; het vertelt je dat de output incompleet is.

Bekijk wat Kompose heeft gegenereerd:

ls -la *.yaml
# Verwachte output:
# db-deployment.yaml
# db-service.yaml
# db-data-persistentvolumeclaim.yaml
# wordpress-deployment.yaml
# wordpress-service.yaml
# wp-content-persistentvolumeclaim.yaml

Pas deze bestanden nog niet toe. Ze moeten eerst gehard worden. Ook de officiële Kubernetes-documentatie zegt dat je moet "review and edit" voordat je apply draait.

Checkpoint: je hebt zes YAML-bestanden. Geen ervan hoort as-is te worden toegepast.

Stap 3: maak een namespace en secrets aan

Begin met een dedicated namespace en verplaats gevoelige waarden uit plaintext.

kubectl create namespace wordpress

Maak Secrets aan voor alle gevoelige waarden uit Stap 1:

kubectl create secret generic wordpress-secrets \
  --from-literal=db-password="$(openssl rand -base64 24)" \
  --from-literal=db-root-password="$(openssl rand -base64 24)" \
  --namespace=wordpress

Maak een ConfigMap voor niet-gevoelige configuratie:

# wordpress-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: wordpress-config
  namespace: wordpress
data:
  WORDPRESS_DB_HOST: "db"
  WORDPRESS_DB_NAME: "wordpress"
  WORDPRESS_DB_USER: "wordpress"
kubectl apply -f wordpress-config.yaml

Bewerk nu beide Deployment-bestanden om hiernaar te verwijzen in plaats van naar plaintext env:-waarden. Vervang in wordpress-deployment.yaml het env:-blok dat Kompose genereerde:

# Vervang Kompose's plaintext env: blok hiermee
envFrom:
- configMapRef:
    name: wordpress-config
env:
- name: WORDPRESS_DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: wordpress-secrets
      key: db-password

En in de MySQL Deployment:

env:
- name: MYSQL_ROOT_PASSWORD
  valueFrom:
    secretKeyRef:
      name: wordpress-secrets
      key: db-root-password
- name: MYSQL_PASSWORD
  valueFrom:
    secretKeyRef:
      name: wordpress-secrets
      key: db-password
- name: MYSQL_DATABASE
  value: "wordpress"
- name: MYSQL_USER
  value: "wordpress"

Base64-encoding (zoals gebruikt in Secret YAML-manifesten) is encoding, geen encryptie. Voor productie-GitOps-workflows kun je kijken naar Sealed Secrets of HashiCorp Vault.

Checkpoint: er staan geen gevoelige waarden meer als plaintext in je YAML-bestanden.

Stap 4: fix storage met goede PersistentVolumeClaims

Kompose maakt wel PVC-manifesten, maar zonder de belangrijkste velden. De gegenereerde PVCs verwijzen naar geen StorageClass en geen opslaggrootte. Voor een dieper begrip van PVs, PVCs en StorageClasses, zie Kubernetes PersistentVolumes en PersistentVolumeClaims.

Check eerst welke StorageClasses je cluster aanbiedt:

kubectl get storageclass
# Voorbeeldoutput op een cloudcluster:
# NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
# standard (default)   kubernetes.io/gce-pd    Delete          WaitForFirstConsumer
# premium-rwo          pd.csi.storage.gke.io   Delete          WaitForFirstConsumer

Vervang de gegenereerde PVC voor de database door een goed gespecificeerde:

# db-data-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
  namespace: wordpress
spec:
  accessModes:
    - ReadWriteOnce            # single pod read/write, correct voor databases
  storageClassName: standard   # moet overeenkomen met een StorageClass in je cluster
  resources:
    requests:
      storage: 20Gi

Doe hetzelfde voor wp-content:

# wp-content-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-content
  namespace: wordpress
spec:
  accessModes:
    - ReadWriteOnce            # prima voor single-replica WordPress
  storageClassName: standard
  resources:
    requests:
      storage: 10Gi

Als je StorageClass de hostPath-provisioner gebruikt (standaard op Minikube), dan staat data in een temp-directory op de node en gaat die verloren bij rescheduling. Dat is alleen voor development.

Checkpoint: draai kubectl get storageclass en bevestig dat je gekozen StorageClass bestaat. Elke PVC specificeert een accessMode, een storageClassName en een storage-grootte.

Stap 5: voeg resource requests en limits toe

Kompose genereert geen resources-blok. Zonder resource requests heeft de scheduler geen capaciteitsgarantie voor je pods. Zonder limits kan een op hol geslagen container een hele node opeten. Voor de volledige mechanica, zie Kubernetes resource requests en limits.

Voeg resource-blokken toe aan elke container in elke Deployment. Dit zijn startwaarden; stem ze af met kubectl top pods of VPA in aanbevelingsmodus zodra je productiedata hebt.

WordPress-container:

resources:
  requests:
    cpu: "100m"       # scheduling-garantie
    memory: "128Mi"
  limits:
    cpu: "500m"       # kernel throttlet hierboven
    memory: "256Mi"   # kernel OOMKillt hierboven

MySQL-container:

resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1000m"
    memory: "512Mi"

CPU throttlet (het proces wordt trager). Memory OOMKillt (het proces wordt gekilld). Stel memory limits conservatief maar realistisch in. Een te krappe limit veroorzaakt frequente restarts; een te ruime verspilt node-capaciteit.

Checkpoint: elke container in elke Deployment heeft zowel requests als limits.

Stap 6: voeg health probes toe

Kompose genereert geen health probes. Zonder probes kan Kubernetes een vastgelopen container niet detecteren, kan het geen verkeer omleiden bij ongezonde pods, en kan het nieuwe pods niet veilig verifiëren tijdens rolling updates. Voor probe-types, mechanismen en timing, zie hoe je Kubernetes health probes configureert.

Voeg probes toe aan de WordPress-container:

# WordPress health probes
startupProbe:             # blokkeert liveness + readiness tot boot klaar is
  httpGet:
    path: /wp-login.php
    port: 80
  failureThreshold: 30   # 30 x 10 = 300 seconden maximaal opstarttijd
  periodSeconds: 10
livenessProbe:            # herstart container als hij vastloopt
  httpGet:
    path: /wp-login.php
    port: 80
  periodSeconds: 30
  failureThreshold: 3
readinessProbe:           # verwijdert pod uit Service endpoints als ongezond
  httpGet:
    path: /wp-login.php
    port: 80
  periodSeconds: 10
  failureThreshold: 3

Plugin-zware WordPress-installaties kunnen meer dan 60 seconden nodig hebben bij de eerste boot. De startup probe met een 300-seconden-venster houdt daar rekening mee zonder snelle boots af te straffen.

Voeg een TCP-probe toe aan de MySQL-container (MySQL heeft geen HTTP-endpoint):

# MySQL health probes
startupProbe:
  tcpSocket:
    port: 3306
  failureThreshold: 30
  periodSeconds: 10
livenessProbe:
  tcpSocket:
    port: 3306
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  tcpSocket:
    port: 3306
  periodSeconds: 10
  failureThreshold: 3

Checkpoint: elke container heeft een startupProbe, livenessProbe en readinessProbe.

Stap 7: vervang depends_on door init containers

Docker Compose depends_on zorgt ervoor dat de database-container start voor WordPress. Kubernetes heeft geen equivalent veld. Kompose laat het stilletjes vallen. Zonder vervanging probeert WordPress verbinding te maken met MySQL voordat MySQL klaar is, wat resulteert in Error establishing a database connection bij de eerste deploy.

De oplossing: voeg een init container toe aan de WordPress Deployment die wacht tot MySQL verbindingen accepteert.

# Voeg toe aan de WordPress Deployment spec.template.spec
initContainers:
- name: wait-for-mysql
  image: mysql:8.0     # hergebruik het MySQL-image, dat heeft mysqladmin
  command:
  - sh
  - -c
  - |
    until mysqladmin ping -h db --silent; do
      echo "Waiting for MySQL..."
      sleep 3
    done
  env:
  - name: MYSQL_PWD
    valueFrom:
      secretKeyRef:
        name: wordpress-secrets
        key: db-root-password

Init containers draaien sequentieel, tot voltooiing, voordat de main containers starten. Zodra mysqladmin ping slaagt, start WordPress met een werkende databaseverbinding.

Checkpoint: deploy de WordPress-pod en check kubectl describe pod <wordpress-pod> -n wordpress. Je zou de wait-for-mysql init container moeten zien met status Completed voordat de main container start.

Stap 8: kies de juiste deployment strategy

Dit is de stap die mensen overslaan, om er vervolgens uren aan debugging te besteden.

De standaard Kubernetes deployment strategy is RollingUpdate: maak een nieuwe pod aan voordat de oude wordt gestopt. Met ReadWriteOnce-PVCs (het meest voorkomende type voor cloud block storage) kan maar één pod tegelijk het volume mounten. Tijdens een rolling update hangt de nieuwe pod in Pending met een Multi-Attach error omdat de oude pod het volume nog vasthoudt.

Voor single-replica workloads met ReadWriteOnce-PVCs, gebruik Recreate:

# In zowel wordpress-deployment.yaml als db-deployment.yaml
spec:
  strategy:
    type: Recreate    # stop oude pod voordat nieuwe pod wordt aangemaakt

Dit veroorzaakt korte downtime tijdens updates. Voor een single-replica WordPress + MySQL-setup is die afweging acceptabel en voorkomt het de Multi-Attach-deadlock volledig.

Wil je zero-downtime voor WordPress, dan heb je ReadWriteMany-storage nodig (NFS, AWS EFS, Azure Files) zodat meerdere pods hetzelfde volume kunnen mounten. Dat is een andere architectuur dan wat Docker Compose je geeft.

Checkpoint: beide Deployments hebben strategy.type: Recreate.

Stap 9: apply en verifieer

Pas alle manifesten toe op de wordpress-namespace:

kubectl apply -f . -n wordpress

Monitor de pod-status:

kubectl get pods -n wordpress -w
# Verwacht: init container draait eerst, daarna bereiken beide pods Running/Ready

Check PVC-binding:

kubectl get pvc -n wordpress
# Verwacht: STATUS = Bound voor beide PVCs

Als een PVC in Pending blijft hangen, is de meest waarschijnlijke oorzaak een ontbrekende of niet-matchende StorageClass. Verifieer met kubectl describe pvc <naam> -n wordpress en vergelijk de storageClassName met kubectl get storageclass.

Wacht op de WordPress-rollout:

kubectl rollout status deployment/wordpress -n wordpress
# Verwacht: "deployment 'wordpress' successfully rolled out"

Open de WordPress-setup-wizard:

kubectl port-forward svc/wordpress 8080:80 -n wordpress
# Open http://localhost:8080 in je browser

Checkpoint: je ziet de WordPress-installatiewizard. De databaseverbinding werkt. Beide PVCs zijn Bound.

Drie misvattingen die mensen in de problemen brengen

"Kompose-output is productiewaardig"

Kompose is scaffolding. De officiële Kompose-documentatie stelt dat conversies "not always 1-to-1" zijn. Gegenereerde manifesten missen resource limits (risico: pod-eviction en node-uithongering), health probes (risico: verkeer naar dode pods), en secrets-afhandeling (risico: wachtwoorden in Git). Stappen 3 tot en met 8 van deze tutorial bestaan volledig omdat Kompose-output incompleet is.

"Docker Compose-volumes mappen direct op Kubernetes-volumes"

Docker Compose named volumes zijn host-lokale storage. Kubernetes biedt meerdere volume-types, en Kompose maakt PVCs aan die mogelijk verwijzen naar een StorageClass die niet bestaat in je cluster. Zonder geldige StorageClass hangt de PVC voor altijd in Pending zonder duidelijke foutmelding. Op Minikube slaat de standaard hostPath-provisioner data op in /tmp, wat verloren gaat bij rescheduling.

"Docker Compose-networks mappen op Kubernetes-namespaces"

Ze zijn fundamenteel verschillend. Docker Compose-networks zijn per-project bridge networks voor DNS-isolatie. Kubernetes gebruikt flat networking: elke pod kan elke andere pod bereiken via IP-adres over het hele cluster. Namespaces zijn administratieve grenzen (RBAC, resource quota's), geen netwerkisolatie. Heb je netwerkisolatie nodig na de migratie, dan heb je NetworkPolicy-objecten nodig.

Productie-hardening checklist

Verifieer elk punt voordat je dit in productie draait:

  • [ ] Resource requests en limits op elke container
  • [ ] Liveness, readiness en startup probes op elke container
  • [ ] Alle gevoelige waarden in Kubernetes Secrets, geen plaintext in YAML gecommit naar Git
  • [ ] StorageClass bevestigd met kubectl get storageclass
  • [ ] PVC reclaim policy ingesteld op Retain voor database-volumes (kubectl patch pv <naam> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}')
  • [ ] Deployment strategy komt overeen met PVC access mode (Recreate voor ReadWriteOnce)
  • [ ] Dedicated ServiceAccount per applicatie met minimale RBAC
  • [ ] Ingress met TLS-terminatie voor externe toegang (zie cert-manager NGINX-tutorial)
  • [ ] NetworkPolicy als inter-service-isolatie vereist is

Wat je geleerd hebt

Je begon met een Docker Compose-bestand en eindigde met productiegeharde Kubernetes-manifesten. Het belangrijkste inzicht: Kompose handelt de structurele conversie af, maar de beveiliging, betrouwbaarheid en operationele gaten zijn voor jou om te dichten.

De concepten die je hier hebt toegepast (Secrets, PVCs, init containers, health probes, deployment strategy) zijn dezelfde die je gebruikt voor elke Kubernetes-workload, niet alleen voor Docker Compose-migraties. Specifiek voor productie-WordPress op Kubernetes bundelt de Bitnami WordPress Helm chart deze patronen in een enkel, onderhouden pakket. Het handmatige pad in deze tutorial leert je wat die chart onder de motorkap doet.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.