← Terug naar tutorials

Van Docker naar Kubernetes: een praktisch migratiepad voor bestaande Compose-projecten

docker-composekubernetesdevopsmigratiecontainershelmci-cdobservability

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

  1. Voor wie is dit?
  2. Wat verandert er conceptueel?
  3. Voorbeeld Compose-project
  4. Voorbereiding: tools en cluster
  5. Stap 1: Images bouwen en publiceren
  6. Stap 2: Compose naar Kubernetes mappen (concepten)
  7. Stap 3: Eerste migratie met Kompose (snelle start)
  8. Stap 4: Handmatig “productiewaardig” maken
  9. Stap 5: Deployen, debuggen en verifiëren
  10. Stap 6: Migratiepad in fases (realistische aanpak)
  11. Veelvoorkomende valkuilen
  12. Volgende stap: Helm en GitOps

Voor wie is dit?


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:

Belangrijke verschuivingen:


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:


Voorbereiding: tools en cluster

Lokale tools

Installeer:

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

ComposeKubernetesOpmerking
serviceDeployment/StatefulSet + ServiceStateless → Deployment, stateful → StatefulSet
ports: “8080:8080”Service + (Ingress/NodePort/LB)K8s expose is losgekoppeld
environmentConfigMap/Secret + envFrom/envSecrets voor wachtwoorden/keys
depends_onReadiness probes + initContainers (soms)K8s startvolgorde is niet gegarandeerd
volumesPVC + StorageClassBind mounts zijn anti-pattern in clusters
restart: alwaysdefault behaviorK8s herstart pods automatisch
networksK8s cluster netwerk + namespacesService 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:

Deployen

kubectl apply -f .
kubectl get pods
kubectl get svc

Waarom dit slechts een start is

Kompose:

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:

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:

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:

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:

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:

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

Fase 1: Kubernetes draaien zonder Ingress

Fase 2: Externe toegang

Fase 3: CI/CD

Fase 4: Observability en policies


Veelvoorkomende valkuilen

  1. “depends_on” missen

    • Oplossing: readiness probes + retries/backoff in de app. Kubernetes garandeert geen startvolgorde.
  2. Database als Deployment

    • Oplossing: StatefulSet + PVC. Of beter: managed database buiten het cluster.
  3. Bind mounts willen gebruiken

    • In clusters is lokale disk per node niet betrouwbaar. Gebruik PVC’s of object storage.
  4. Geen resource limits

    • Kan leiden tot instabiliteit en noisy neighbor-problemen.
  5. Secrets in Git

    • Gebruik externe secret managers of minstens sealed-secrets/sops. In deze tutorial staan secrets in plain manifests voor leerdoeleinden.
  6. 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.

Een logische volgende stap is:


Samenvatting

Je hebt nu een praktisch migratiepad:

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