Docker Compose ist für viele der erste Ort, an dem mehrere Container auf einer Maschine zusammenlaufen. Man deklariert Services, Images, Ports, Umgebungsvariablen, Volumes und manchmal Abhängigkeiten. Compose baut ein kleines privates Netzwerk und startet Container in einer sinnvollen Reihenfolge. Für lokale Entwicklung und kleine Deployments reicht das oft.

Kubernetes verlangt eine andere Form, aber die Denkarbeit ist vertraut. Man beschreibt weiterhin Anwendungen, wie sie miteinander sprechen und welche Konfiguration sie brauchen. Die Substantive ändern sich. Die Gewohnheit, in Services und Abhängigkeiten zu denken, bleibt.

Dieser Beitrag richtet sich an die Phase, in der Compose bereits funktioniert und Kubernetes sich wie eine größere Variante derselben Idee anfühlt — mit mehr YAML und mehr Fragen.

Was Compose gratis mitliefert

Eine typische docker-compose.yml bündelt mehrere Themen:

  • Services — benannte Workloads mit Image und Befehl.
  • Ports — Host-Ports veröffentlichen oder Container-Ports exposen.
  • Environment — Inline-Variablen oder .env-Dateien.
  • Volumes — Bind Mounts, benannte Volumes oder tmpfs.
  • Networks — Standard-Bridge oder eigene Netze; DNS-Namen entsprechen den Service-Namen.
  • depends_on — Hinweise zur Startreihenfolge (in älteren Compose-Versionen nicht gesundheitsbewusst).

Compose läuft auf einem Docker-Host (oder einem kleinen Swarm). Alles teilt Kernel und Netzwerkmodell dieses Hosts. Kubernetes verteilt Arbeit über einen Cluster: Scheduler, Nodes, API-Objekte, Controller und label-basierte Service-Erkennung.

Ziel der Migration ist nicht, jedes Compose-Feature am ersten Tag in Kubernetes nachzubauen. Ziel ist, das zu erhalten, was in Compose wichtig war — lauffähige Apps, erreichbare Ports, Konfiguration, Speicher — und zu akzeptieren, was Kubernetes anders macht.

Die Standard-Zuordnung: Service → Deployment + Service

In Kubernetes ist eine dauerhaft laufende Anwendung meist ein Deployment (gewünschte Replikate, Rollout-Strategie, Pod-Template), das Pods erzeugt (die eigentlichen Container). Andere Workloads erreichen sie über einen Service (stabile Cluster-IP und DNS-Name, Lastverteilung auf Pod-Endpunkte).

Eine grobe Übersetzungstabelle:

Compose-KonzeptKubernetes-Einstieg
services.webDeployment web + Service web
image:containers[].image im Pod-Template
ports:containerPort + Service ports
environment:Env-Variablen oder ConfigMap-/Secret-Refs
volumes:Volumes im Pod-Spec + PVC / ConfigMap / Secret
depends_on:Init-Container, Probes oder Retry in der App (kein exaktes Pendant)
Service-Name als Hostnamehttp://web über Service-DNS im gleichen Namespace

Der Compose-Service-Name api wird im Cluster zum DNS-Namen api, wenn ein Service api existiert. Diese Ähnlichkeit hilft Einsteigern. Der Unterschied: Kubernetes-DNS gilt clusterweit pro Namespace, nicht als Docker-Bridge-Netz in einer Datei.

Hier ein minimaler Compose-Service:

services:
  web:
    image: ghcr.io/example/shop-web:1.2.0
    ports:
      - "8080:8080"
    environment:
      API_URL: http://api:3000
      LOG_LEVEL: info
    depends_on:
      - api

Ein minimales Kubernetes-Pendant trennt Workload und Netzwerk:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: ghcr.io/example/shop-web:1.2.0
          ports:
            - containerPort: 8080
          env:
            - name: API_URL
              value: "http://api:3000"
            - name: LOG_LEVEL
              value: info
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - port: 8080
      targetPort: 8080

Beachten, was sich verschoben hat. Compose fasste „diesen Container starten“ und „diesen Port exposen“ in einem Service-Block zusammen. Kubernetes trennt wie viele Pods laufen (Deployment) von wie Traffic sie findet (Service). Labels (app: web) verbinden beides. Stimmt der Service-Selector nicht mit den Pod-Labels überein, läuft die App ohne Endpunkte — ein klassischer Fehler in der ersten Woche.

Netzwerke: gleiche Idee, andere Umsetzung

In Compose lösen sich Services im gleichen Netz per Name auf. In Kubernetes tun das Pods im Cluster-Netz über ClusterIP-Services (Standardtyp). http://api im Namespace shop zeigt auf den Service vor Pods mit Label app: api.

Womit Compose weniger vorbereitet:

  • Namespaces teilen Objekte und DNS. api.shop.svc.cluster.local ist explizit; Kurznamen funktionieren im gleichen Namespace.
  • NetworkPolicies können Traffic zwischen Pods einschränken. Compose hat kein Pendant außer eigener Firewall-Arbeit.
  • Ingress (oder Gateway API) ersetzt oft ports: "80:8080" für HTTP von außerhalb. Compose veröffentlicht auf den Host; Kubernetes routet häufig über Controller und TLS-Terminierung.
  • Host-Netzwerk gibt es in Kubernetes, ist aber die Ausnahme. Die meisten Apps nutzen normales Pod-Netzwerk.

Für eine erste Migration langweilig bleiben: ein Namespace, ClusterIP-Services, Ingress erst bei Bedarf für externes HTTP. Jede Compose-Port-Regel am ersten Tag nachzubauen, erzeugt nur Lärm.

Volumes: benannte Volumes übertragen sich nicht wörtlich

Compose-Volumes sind praktisch, weil Docker Speicher auf diesem Host verwaltet. Kubernetes ist node-orientiert, aber cluster-scoped: ein Pod kann auf einem anderen Node neu geplant werden — lokale Bind Mounts verhalten sich anders.

Typische Zuordnungen:

ComposeKubernetes
Benanntes VolumePersistentVolumeClaim + StorageClass
Bind Mount ./datahostPath (nur Dev), oder Daten anders synchronisieren
Config-DateiConfigMap- oder Secret-Volume
Anonymes VolumeemptyDir (ephemer)

Eine Datenbank in Compose nutzte oft ein benanntes Volume auf dem Laptop. In Kubernetes will man eine PersistentVolumeClaim mit StorageClass, die zum Cluster passt (Cloud-Disk, Local-Path-Provisioner in kind, …). ReadWriteOnce-Volumes hängen zeitweise an einem Node — das beeinflusst Scheduling und Failover, woran Compose selten erinnerte.

Für stateless Apps zuerst ohne persistentes Volume. Für Postgres o. Ä. Speicher planen, bevor Produktionsdaten wandern.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

Dann Claim im Deployment-Pod-Template unter volumes und volumeMounts referenzieren. Größe und StorageClass hängen vom Cluster ab; das Muster zählt.

Umgebungsvariablen und Secrets

Compose environment: und env_file: passen zu Kubernetes-Env-Variablen — bis Rotation, Audit oder unterschiedliche Werte pro Umgebung relevant werden.

Empfehlungen für den Einstieg:

  • Unkritische Config → ConfigMap, als Env oder Dateien.
  • Passwörter und Tokens → Secret (realistisch: RBAC und Verschlüsselung at rest zählen mehr als das Wort „Secret“).
  • Umgebungsspezifische URLs nicht ins Image backen; gleiches Image-Tag mit verschiedenen ConfigMaps.
env:
  - name: LOG_LEVEL
    valueFrom:
      configMapKeyRef:
        name: shop-config
        key: LOG_LEVEL
  - name: DATABASE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: shop-secret
        key: DATABASE_PASSWORD

In Compose reichte oft ein Neustart für geänderte .env — je nach Aufruf. Kubernetes-Pods laden Env aus geänderter ConfigMap nicht automatisch neu, außer man startet Pods neu oder nutzt gemountete Dateien mit Reload in der App. Config-Änderungen brauchen Rollout-Planung.

depends_on ist kein Kubernetes-Feature

Compose depends_on suggeriert Startreihenfolge. Es garantiert nicht, dass die Abhängigkeit bereit ist — nur dass der Container in vielen Setups startete. Kubernetes hat kein direktes depends_on.

Optionen:

  • Init-Container laufen vor App-Containern zu Ende (Migrationen, Wait-Skripte).
  • Readiness-Probes halten Traffic zurück, bis der Pod bedienen kann.
  • Retries in der Anwendung beim Aufruf von Abhängigkeiten (weiterhin sinnvoll).

Bei Datenbank-Abhängigkeit soll die App kurze Verbindungsfehler beim Rollout tolerieren. Nur Startreihenfolge macht Deploys in Compose und Kubernetes fragil.

kompose und andere Konverter (optional)

kompose kann eine Compose-Datei in Kubernetes-Manifeste übersetzen. Das ist als Skizze nützlich, nicht als fertige Produktions-Config. Generiertes YAML nutzt oft ein Replikat, nackte Deployments und hostPath-Volumes, die ein echter Cluster-Review nicht bestehen.

Bei kompose:

  • Jede generierte Datei lesen.
  • Resource Requests und Limits ergänzen.
  • hostPath durch PVCs ersetzen, wo Daten bleiben müssen.
  • Probes, konsistente Labels, Ingress nur wo nötig.
  • Secrets aus plain env in generierten Dateien herauslösen.

kompose-Output wie einen ersten Entwurf behandeln — von jemandem, der die eigenen Produktionsstandards noch nicht kennt.

Was ehrlich schwieriger wird

Was in Compose leicht wirkte, wird in Kubernetes Betriebsarbeit:

  • Lokale Iterationdocker compose up auf einer Maschine ist schnell. Kubernetes-Schleifen brauchen Cluster (kind, minikube, Cloud), Image-Push ins Registry, kubectl apply.
  • Speicher und Backups — PVCs, Snapshots, Restore-Übungen liegen bei einem selbst.
  • Observability — Logs pro Container; Aggregation mit kubectl logs, Loki oder Plattform-Stack.
  • Config-Drift — viele Objekte; GitOps oder diszipliniertes Apply hilft.
  • Debugging — mehr Schichten (Deployment → ReplicaSet → Pod → Container). Vorteil: konsistenter Status und Events, wenn man sie lesen lernt.

Das heißt nicht, Kubernetes sei falsch. Es heißt, die Migration soll eine Lernkurve respektieren — YAML-Übersetzung ist nicht der ganze Job.

Schrittweise Migration, die wirklich fertig wird

Big-Bang „freitags die ganze Compose-Datei konvertieren“ bleibt oft hängen. Ein besserer Pfad:

1. Einen stateless Service wählen.
web oder api migrieren, nicht Datenbank, Worker und Cache auf einmal.

2. Eigenen Namespace anlegen.
shop-dev oder shop-staging hält Experimente von anderen Workloads fern.

3. Eine Weile parallel laufen.
Compose für lokale Dev behalten, wenn es hilft. Kubernetes-Version in Staging. Verhalten, Logs und Env vergleichen.

4. Echte Registry einführen.
Nodes ziehen Images aus einer Registry, die der Cluster erreicht. imagePullPolicy und Tags zählen.

5. Ingress, wenn interne Services stehen.
curl aus einem anderen Pod zu http://api beweisen, bevor HTTP nach außen geht.

6. Stateful Services zuletzt.
Postgres, Redis mit Persistenz oder Queues nach PVCs, Backups und Recovery verstehen.

7. Mapping dokumentieren.
Kurze Tabelle im Repo („Compose worker → Deployment worker, Queue-URL aus ConfigMap shop-config“) hilft beim Incident.

kubectl create namespace shop-staging
kubectl apply -f k8s/web/ -n shop-staging
kubectl get deploy,svc,pods -n shop-staging
kubectl run curl --rm -it --image=curlimages/curl --restart=Never -n shop-staging -- \
  curl -sS http://api:3000/health

Der letzte Befehl ist ein einfacher Smoke-Test: erreicht etwas im Cluster den Service-DNS-Namen und Port?

Checkliste, bevor die Migration „fertig“ heißt

  • Deployment-Replikate und Rollout-Strategie passen (nicht immer 1).
  • Service-Selector passen zu Pod-Labels; Endpunkte existieren (kubectl get endpoints).
  • Config und Secrets nicht im Klartext im Repo.
  • Probes für alles, was Traffic bekommt.
  • Resource Requests (und Limits wo sinnvoll) für planbares Scheduling.
  • Images mit festen Tags, nicht nur latest, jenseits von Spielzeug-Clustern.
  • Klarheit, wie man bei Fehlern Logs liest und Pods beschreibt.

Abschluss

Der Weg von Docker Compose zu Kubernetes ist weniger Auswendiglernen jedes API-Feldes als Trennen von Themen, die Compose zusammengefasst hat: Workload, Netzwerk, Config, Speicher. Der erste Erfolg ist ein Service zuverlässig hinter einem Service-DNS-Namen in einem kontrollierten Namespace. Jeder weitere Service nutzt dasselbe Muster. Schrittweise schlägt heroisch — besonders wenn der Cluster neu ist und die Compose-Datei gestern noch gut lief.