Docker und Kubernetes: Ein praktischer Leitfaden fuer moderne Entwickler
Ein auf das Wesentliche konzentrierter Leitfaden zu Containerisierung, Dockerfile-Best-Practices, Kubernetes-Grundlagen und der Frage, wann Sie tatsaechlich einen Orchestrator brauchen.
Jeder Entwickler stoesst irgendwann auf dieselbe Wand. Sie schreiben Code auf Ihrem Rechner, er funktioniert einwandfrei, Sie pushen ihn ins Staging, und er explodiert mit einem kryptischen Fehler ueber eine fehlende Systemabhaengigkeit oder eine andere Bibliotheksversion. Das klassische 'Funktioniert auf meinem Rechner'-Problem hat die Softwareentwicklung jahrzehntelang verfolgt, und Docker hat es nicht nur geloest - es hat die Loesung so einfach gemacht, dass es jetzt keine Ausrede mehr gibt, sie nicht zu nutzen.
Aber Docker loest nur das Verpackungsproblem. Sobald Sie Ihre Anwendung containerisiert haben, muessen Sie sie immer noch in der Produktion betreiben - potenziell ueber mehrere Server hinweg, mit Load Balancing, Null-Ausfallzeit-Deployments, Health Checks und automatischer Wiederherstellung. Hier kommt Kubernetes ins Spiel. Und hier verlieren sich die meisten Entwickler in der Komplexitaet, denn Kubernetes fuehrt eine voellig neue Abstraktionssprache ein, die Zeit braucht, um verinnerlicht zu werden.
Dieser Leitfaden durchschneidet das Rauschen. Er behandelt, was Container tatsaechlich unter der Haube sind, wie Sie effiziente und sichere Dockerfiles schreiben, die Kubernetes-Konzepte, die Sie fuer echte Anwendungen kennen muessen, die Kompromisse zwischen Docker Compose und Kubernetes fuer die lokale Entwicklung und - am wichtigsten - wann Sie zu welchem Tool greifen und wann Sie es in Ruhe lassen sollten.
Was Container tatsaechlich sind
Ein Container ist keine leichtgewichtige virtuelle Maschine. Dies ist das haeufigste Missverstaendnis und fuehrt zu falschen mentalen Modellen. Eine virtuelle Maschine fuehrt ein vollstaendiges Gastbetriebssystem auf einem Hypervisor aus, mit eigenem Kernel, eigener Speicherzuweisung und eigenen Geraetetreibern. Ein Container teilt sich den Host-Kernel und laeuft als isolierter Userspace-Prozess. Die Isolation wird durch Linux-Kernel-Funktionen bereitgestellt - Namespaces fuer Prozessisolation, Cgroups fuer Ressourcenlimits und Overlay-Dateisysteme fuer effiziente Image-Layer.
Diese Unterscheidung ist wichtig, weil sie das Verhalten erklaert, das Sie beobachten werden. Container starten in Millisekunden, weil kein Kernel gebootet werden muss. Sie verbrauchen weniger Speicher, weil es keinen doppelten Kernel und keine redundanten Systemprozesse gibt. Aber sie bedeuten auch, dass ein unter Linux laufender Container keine andere Kernelversion als der Host ausfuehren kann und dass ein Windows-Container einen Windows-Host benoetigt (oder eine Hyper-V-Linux-VM auf aelteren Versionen). Unter macOS fuehrt Docker Desktop Linux-Container aus diesem Grund in einer leichtgewichtigen VM aus.
Ein Image ist die schreibgeschuetzte Vorlage - ein Schnappschuss eines Dateisystems plus Metadaten. Ein Container ist eine laufende Instanz dieses Images mit einer beschreibbaren Schicht darueber. Sie koennen ein Image einmal erstellen und Dutzende von Containern daraus ausfuehren. Dies ist die grundlegende Betriebseinheit in der Docker-Welt, und ein klares Verstaendnis davon macht alles andere einfacher.
Dockerfile-Best-Practices
Ein Dockerfile ist ein Rezept zum Erstellen eines Images. Jede Anweisung erzeugt einen neuen Layer, und Layer werden zwischengespeichert. Das bedeutet, dass die Reihenfolge der Anweisungen direkt die Build-Geschwindigkeit, die Image-Groesse und die Sicherheit beeinflusst. Hier sind die Prinzipien, die in echten Projekten am wichtigsten sind.
Layer nach Aenderungshaeufigkeit ordnen
Docker speichert jeden Layer nach dem Erstellen zwischen. Wenn sich ein Layer seit dem letzten Build nicht geaendert hat, verwendet Docker die zwischengespeicherte Version wieder. Das bedeutet, Sie sollten Anweisungen, die sich selten aendern, an den Anfang stellen und Anweisungen, die sich haeufig aendern, an das Ende. Systemabhaengigkeiten (apt-get, apk add) aendern sich fast nie. Anwendungsabhaengigkeiten (npm install, pip install) aendern sich, wenn Sie Ihre Lockfile aktualisieren. Anwendungsquellcode aendert sich bei jedem Commit.
# Bad: source code before dependencies
FROM node:20-alpine
WORKDIR /app
COPY . . # busts the cache for everything below
RUN npm ci # runs on every build, even if package.json did not change
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Good: stable-first layer ordering
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # cached unless package.json changes
COPY . . # only the source changes bust this layer
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]Der Unterschied ist dramatisch. Das schlechte Dockerfile baut alle Abhaengigkeiten bei jedem Commit neu auf. Das gute baut Abhaengigkeiten nur neu auf, wenn sich die Lockfile aendert, was typischerweise einmal pro Pull Request und nicht bei jedem Commit der Fall ist. Bei einem Node.js-Projekt mit 500 Abhaengigkeiten kann dies zwei Minuten pro Build einsparen.
Multi-Stage-Builds
Multi-Stage-Builds ermoeglichen es Ihnen, mit einem Dockerfile Ihre Anwendung zu bauen und ein minimales Laufzeit-Image zu erzeugen. Die Build-Stage enthaelt Compiler, Entwicklungsabhaengigkeiten und Build-Tools. Die Laufzeit-Stage kopiert nur die kompilierte Ausgabe. Dies haelt Produktions-Images klein und reduziert die Angriffsflaeche.
# Build stage
FROM node:20-alpine AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage — starts from a fresh, minimal base
FROM node:20-alpine AS runner
WORKDIR /app
# Only what is needed to run
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/package.json ./
COPY --from=builder /build/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]Die Laufzeit-Stage enthaelt weder den TypeScript-Compiler, die Quelldateien noch Entwicklungsabhaengigkeiten. Fuer eine typische Anwendung schrumpft das Image dadurch von 800 MB auf unter 200 MB. Die COPY --from=builder-Syntax ist die entscheidende Erkenntnis - sie zieht Dateien aus einer vorherigen Stage, ohne die Layer dieser Stage mitzunehmen.
Als Nicht-Root-Benutzer ausfuehren
Container laufen standardmaessig als Root. Dies ist ein Sicherheitsrisiko: Wenn ein Angreifer Ihre Anwendung ausnutzt, hat er Root-Zugriff innerhalb des Containers. Die Behebung ist eine einzelne Zeile in Ihrem Dockerfile, die zu einem Nicht-Root-Benutzer wechselt. Die meisten Basis-Images enthalten zu diesem Zweck einen node- oder nobody-Benutzer.
Ueber die Sicherheit hinaus verbessern Multi-Stage-Builds und die richtige Layer-Reihenfolge auch die CI/CD-Pipeline-Geschwindigkeit. Jede Minute, die bei einem Image-Build eingespart wird, ist eine Minute, die Ihre Entwickler nicht auf ein Deployment warten. Bei einem Team von zehn Entwicklern, die fuenfmal taeglich deployen, spart das Einsparen von zwei Minuten pro Build ueber sechzig Stunden Entwicklerzeit pro Jahr.
Kubernetes-Grundlagen
Kubernetes ist ein Container-Orchestrator. Es nimmt einen Cluster von Maschinen (Nodes), plant Container darauf, haelt sie am Laufen, handhabt die Vernetzung und bietet eine deklarative API zur Beschreibung des gewuenschten Zustands Ihres Systems. Sie sagen Kubernetes, was Sie wollen - drei Replikate Ihres API-Servers, Port 8080 freigegeben, Rolling-Update-Strategie - und es setzt es um.
Die Lernkurve ist real, weil Kubernetes eine geschichtete Reihe von Abstraktionen einfuehrt. Die drei, mit denen Sie am meisten interagieren werden, sind Pods, Deployments und Services.
Pods
Ein Pod ist die kleinste bereitstellbare Einheit in Kubernetes. Er repraesentiert einen oder mehrere Container, die sich einen Netzwerk-Namespace und Speichervolumes teilen. In der Praxis laufen die meisten Pods mit einem einzigen Container. Sidecar-Muster (ein Hauptcontainer plus ein Logging- oder Proxy-Container) verwenden Multi-Container-Pods, aber fuer den alltaeglichen Anwendungseinsatz werden Sie einen Container pro Pod verwenden.
Sie erstellen Pods selten direkt. Pods sind fluechtig - sie koennen jederzeit beendet und neu geplant werden. Wenn Sie einen Pod manuell erstellen und der Node, auf dem er laeuft, ausfaellt, ist der Pod fuer immer verloren. Hier kommen Deployments ins Spiel.
Deployments
Ein Deployment verwaltet eine Gruppe identischer Pods (ein ReplicaSet). Es handhabt Rolling Updates, Skalierung, Self-Healing und Rollbacks. Dies ist die Ressource, die Sie zum Bereitstellen zustandsloser Anwendungen verwenden werden.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
labels:
app: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/api-server:v1.2.3
ports:
- containerPort: 3000
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 3000
readinessProbe:
httpGet:
path: /ready
port: 3000Dieses Deployment deklariert drei Replikate des API-Servers. Kubernetes stellt sicher, dass immer drei Pods laufen. Wenn ein Pod abstuerzt, erstellt Kubernetes einen Ersatz. Waehrend eines Rolling Updates (Aendern des Image-Tags) ersetzt Kubernetes Pods einen nach dem anderen, was eine Null-Ausfallzeit gewaehrleistet. Der Liveness-Probe teilt Kubernetes mit, wann ein Pod gesund ist; der Readiness-Probe teilt ihm mit, wann ein Pod bereit ist, Datenverkehr zu empfangen.
Services
Pods haben dynamische IP-Adressen. Jedes Mal, wenn ein Pod neu erstellt wird, erhaelt er eine neue IP. Ein Service bietet einen stabilen Netzwerkendpunkt, der den Datenverkehr lastverteilend auf die Pods verteilt, die seinem Selektor entsprechen. Auf diese Weise finden andere Teile Ihres Systems Ihre Anwendung und kommunizieren mit ihr.
apiVersion: v1
kind: Service
metadata:
name: api-server
spec:
selector:
app: api-server
ports:
- port: 80
targetPort: 3000
type: ClusterIPDieser Service bildet Port 80 auf einer stabilen Cluster-internen IP auf Port 3000 der Pods mit dem Label app: api-server ab. Andere Dienste innerhalb des Clusters erreichen ihn ueber den DNS-Namen api-server. Fuer externen Datenverkehr wuerden Sie zusaetzlich zum Service eine LoadBalancer- oder Ingress-Ressource verwenden.
Kubernetes ist keine Plattform, auf die Sie deployen. Es ist eine Plattform, der Sie Ihr Deployment beschreiben. Der Unterschied zwischen imperativ und deklarativ ist die wichtigste mentale Umstellung, die Sie vornehmen koennen.
Lokale Entwicklung: Docker Compose vs. Kubernetes
Der groesste Fehler, den Teams machen, ist die Annahme, sie brauchten Kubernetes fuer die lokale Entwicklung, weil sie es in der Produktion verwenden. Docker Compose und Kubernetes dienen unterschiedlichen Zwecken, und die Wahl des falschen fuer die lokale Arbeit erzeugt unnötige Reibung.
Docker Compose ist fuer die lokale Entwicklung konzipiert. Es laeuft auf einem einzigen Rechner, startet Container in Sekunden und hat ein einfaches YAML-Format, das direkt dem entspricht, was Sie brauchen: einen Webserver, eine Datenbank, eine Redis-Instanz und vielleicht einen Queue-Worker. Sie definieren die Dienste, und docker compose up bringt alles online, mit Logs, die in Ihr Terminal streamen, Ports, die auf localhost gemappt sind, und Hot Reload, das sofort funktioniert.
version: "3.8"
services:
api:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Diese Compose-Datei gibt Ihnen eine funktionierende Entwicklungsumgebung mit Hot Reload, einer lokalen Datenbank mit persistentem Speicher und ordnungsgemaesser Vernetzung zwischen den Diensten. Sie benoetigt etwa dreissig Zeilen YAML und startet in unter zehn Sekunden.
Minikube, Kind und k3s koennen Kubernetes lokal ausfuehren, fuegen aber erheblichen Overhead hinzu. Sie benoetigen mehr Speicher, brauchen laenger zum Starten und fuehren Komplexitaet (Ingress-Controller, Service Meshes, Storage Classes) ein, die Sie schlicht nicht brauchen, wenn Sie an einer einzelnen Funktion arbeiten. Kubernetes lokal auszufuehren ist nuetzlich zum Testen von Kubernetes-spezifischem Verhalten - wie Pod-Eviction-Richtlinien, horizontale Pod-Autoskalierung oder benutzerdefinierte Ressourcendefinitionen - aber es ist kein Ersatz fuer Compose in der taeglichen Entwicklung.
- Verwenden Sie Docker Compose fuer die lokale Entwicklung. Es ist schnell, einfach und bildet direkt die Container ab, die Sie ausfuehren.
- Verwenden Sie Kubernetes (ueber Minikube oder Kind) fuer Integrationstests, wenn Ihre Produktionsinfrastruktur Kubernetes-Funktionen wie ConfigMaps, Secrets oder benutzerdefinierte Controller nutzt.
- Verwenden Sie einen entfernten Entwicklungscluster nur, wenn Sie GPU-Zugriff, spezialisierte Hardware oder eine gemeinsame Staging-Umgebung benoetigen, die die Produktion exakt abbildet.
- Fuehren Sie nicht zwei Kubernetes-Cluster lokal aus, nur weil Sie zwei Umgebungen haben. Compose handhabt das mit einem einzigen --profile-Flag.
- Wenn Ihr Team mehr Zeit mit dem Debuggen von Kubernetes-Konfigurationen als mit dem Schreiben von Anwendungscode verbringt, sind Sie Ihren Scheinwerfern vorausgeeilt. Gehen Sie zurueck zu Compose und fuegen Sie Komplexitaet nur hinzu, wenn der Schmerz, sie nicht zu haben, den Schmerz, sie zu warten, uebersteigt.
Haeufige Fallstricke und wie man sie vermeidet
Selbst nachdem Sie die Konzepte verstanden haben, wiederholen sich bestimmte Fehler in Teams. Hier sind die, die es wert sind, sich einzupruegen, damit Sie sich die zweitaegigen Debugging-Sitzungen sparen koennen.
Image-Tag-Chaos ist das haeufigste Produktionsproblem. Die Verwendung von latest als Image-Tag in einem Kubernetes-Deployment bedeutet, dass Sie nicht sagen koennen, welche Version auf einem bestimmten Node laeuft. Kubernetes zieht ein Image nur, wenn es auf dem Node nicht vorhanden ist, also koennte latest auf einem Node eine andere Version sein als latest auf einem anderen. Verwenden Sie immer semantische Versionstags oder Commit-SHAs. Noch besser: Verwenden Sie einen vollqualifizierten Image-Digest - das ist das Einzige, was garantiert unveraenderlich ist.
Ressourcenanforderungen und -grenzen werden haeufig weggelassen oder willkurlich gesetzt. Wenn Sie keine Anfragen setzen, kann Kubernetes Ihre Pods nicht intelligent planen, und Nodes werden ueberlastet. Wenn Sie keine Grenzen setzen, kann ein Speicherleck in einem Pod andere Pods auf demselben Node zum Absturz bringen. Setzen Sie beides, und verwenden Sie Tools wie den Vertical Pod Autoscaler im Empfehlungsmodus, um sie basierend auf der tatsaechlichen Nutzung zu optimieren.
ConfigMaps und Secrets werden als Umgebungsvariablen oder Dateien bereitgestellt. Umgebungsvariablen sind praktisch, aber jede Aenderung erfordert einen Pod-Neustart, um wirksam zu werden. Dateibasierte Bereitstellungen koennen ohne Neustart aktualisiert werden (der Pod liest den neuen Inhalt beim Zugriff auf die Datei), aber viele Anwendungen cachen die Konfiguration beim Start. Wissen Sie, welches Muster Ihre Anwendung verwendet, und gestalten Sie Ihren Konfigurationsansatz entsprechend.
Persistente Volumes in Kubernetes sind kein Zaubermittel. Ein PersistentVolumeClaim fordert Speicher an, aber die zugrundeliegende Storage-Klasse muss fuer Ihren Cloud-Anbieter konfiguriert sein. Standard-Storage-Klasse verwenden moeglicherweise netzwerkgebundenen Speicher, der andere Leistungsmerkmale als lokale SSDs hat. Wenn Ihre Datenbankleistung wichtig ist, benchmarksen Sie Ihre Storage-Klasse, bevor Sie in Produktion gehen.
Logging und Debugging in Kubernetes ist schwieriger als auf einem einzelnen Server. Pods sind fluechtig, daher verschwinden Logs, wenn ein Pod geloescht wird. Verwenden Sie kubectl logs --tail=50 -f pod-name fuer Live-Tailing, aber fuer das Produktionsdebugging benoetigen Sie eine zentralisierte Logging-Loesung (Loki, Elasticsearch oder einen Cloud-Logging-Dienst). Verwenden Sie kubectl exec -it pod-name -- sh, um einen laufenden Container zu inspizieren, aber denken Sie daran, dass alle Aenderungen, die Sie in einem laufenden Container vornehmen, nach einem Neustart verloren sind.
Brauchen Sie wirklich Kubernetes?
Dies ist die Frage, die niemand stellen will, weil Kubernetes im Lebenslauf eines Ingenieurs gut aussieht und operationelle Reife signalisiert. Aber Kubernetes ist eine Loesung fuer ein spezifisches Problem: mehrere containerisierte Dienste auf mehreren Maschinen mit automatischer Wiederherstellung, Skalierung und Rolling Deployments zu betreiben. Wenn Sie einen oder zwei Dienste auf einem einzigen Server betreiben, ist Kubernetes ueberdimensioniert.
Hier ist ein einfacher Entscheidungsrahmen. Wenn Sie eine einzelne Anwendung bereitstellen, die weniger als zehntausend Anfragen pro Sekunde bedient, werden Sie mit einem einzigen Server mit Docker Compose in der Produktion (ja, Compose funktioniert fuer viele Workloads gut in der Produktion) gut bedient. Fuegen Sie einen Reverse Proxy wie Caddy oder Nginx hinzu, richten Sie automatisierte Backups ein, und Sie haben ein Produktionssystem, das ein einzelner Entwickler vollstaendig verstehen und warten kann.
Wechseln Sie zu Kubernetes, wenn Sie mehrere Dienste haben, die unabhaengig deployed werden muessen, wenn Sie eine dienstspezifische Skalierung benoetigen (Ihr API braucht zehn Replikate, aber Ihr Worker nur zwei), wenn Sie Null-Ausfallzeit-Deployments als Routinevorgang benoetigen oder wenn Ihr Team mindestens eine Person hat, deren Hauptverantwortung die Infrastruktur ist. Bevor diese Bedingungen erfuellt sind, sind die Betriebskosten von Kubernetes - sowohl im Cluster-Management als auch in der kognitiven Belastung der Entwickler - ein Nettominus.
Viele Teams profitieren von einem Mittelweg. Verwenden Sie Docker Compose fuer die lokale Entwicklung und eine verwaltete Container-Plattform wie AWS App Runner, Google Cloud Run oder Fly.io fuer die Produktion. Diese Plattformen bieten Ihnen Container-Deployment, automatisches HTTPS und Skalierung, ohne dass Sie eine Kubernetes-Steuerungsebene verwalten muessen. Sie erhalten die meisten Vorteile der Containerisierung ohne die Kubernetes-Lernkurve.
Die beste Infrastrukturstrategie ist die, die es Ihrem Team ermoeglicht, Funktionen auszuliefern. Docker und Kubernetes sind Werkzeuge, keine Identitaeten. Verwenden Sie sie, wenn sie helfen, und lassen Sie sie weg, wenn sie es nicht tun.
