Docker Multi-Stage-Builds in der Praxis: Produktions-Images von 1 GB auf 10 MB schrumpfen

„Image-Push fehlgeschlagen – Timeout.”
Das war an einem Freitagnachmittag im vergangenen Jahr. Die CI/CD-Pipeline stand rot. Auf dem Bildschirm: ein Go-Image von 980 MB. Ein Ops-Kollege seufzte: „Dein Image ist größer als der Film, den ich mittags geladen habe.”
Dann habe ich Multi-Stage-Builds eingesetzt.
10 MB. Gleiche App, gleiche Funktion – vom 980-MB-Image auf 10 MB. 99 % weniger Volumen; der CI/CD-Push ging von drei Minuten auf drei Sekunden.
In diesem Artikel teile ich praktische Multi-Stage-Tipps: vollständige Dockerfile-Vorlagen für Go, Node.js und Python sowie fünf typische Fehler aus meiner Erfahrung. Wenn Sie Produktions-Images von „aufgebläht” auf „schlank” bringen wollen – lesen Sie weiter.
Warum sind Ihre Images so groß?
Ehrlich gesagt: Die meisten Docker-Images sind aus ähnlichen Gründen aufgebläht.
Früher habe ich so ein Dockerfile geschrieben:
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y golang
COPY . /app
WORKDIR /app
RUN go build -o myapp
CMD ["./myapp"]
Sieht normal aus – aber docker images zeigt: 980 MB.
Das Problem in vier Worten: Behalten, was nötig ist; wegwerfen, was nicht.
Konkret:
- Basis-Image zu groß:
ubuntu:20.04allein ~77 MB, mit Go-Toolchain über 900 MB - Build-Tools bleiben drin: gcc, make, git – in Produktion unnötig
- Cache nicht geleert: apt/apk-Cache bleibt in den Layern
- Redundante Abhängigkeiten: Dev- und Test-Dependencies landen mit im Image
Vergleich: Sie packen Koffer, Schlafsack, Zelt und Kochgeschirr ein – und übernachten im Hotel. Multi-Stage-Builds heißt: nur Kleidung und Toilettenartikel mitnehmen, den Rest zu Hause lassen.
Laut Docker-Dokumentation: typische Go-App unoptimiert ~800 MB–1 GB, optimiert 10–20 MB. Der Unterschied ist enorm.
Kernprinzip des Multi-Stage-Builds
Die Idee ist einfach: Build-Umgebung und Laufzeitumgebung trennen.
Klassische Dockerfiles packen Kompilieren, Packaging und Laufen in ein Image. Multi-Stage erlaubt mehrere FROM-Anweisungen – jede startet eine neue Phase.
Minimalbeispiel:
# Phase 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Phase 2: Laufzeit
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Kernsyntax in zwei Zeilen:
FROM ... AS builder: Phase benennenCOPY --from=builder: Dateien aus der Builder-Phase kopieren
Docker führt alle Phasen nacheinander aus; im finalen Image bleibt nur die letzte Phase. Kompilier-Tools und Dependency-Cache verschwinden.
Laut iximiuz Labs (2026) nutzt Multi-Stage-Build das Layer-Modell: jede FROM-Anweisung startet einen eigenen Kontext. Sie kopieren aus beliebigen Phasen – Unnötiges gelangt nie ins finale Image.
Analogie Renovierung: Phase eins die Baucrew mit Bohrer und Säge; Phase zwei der Einzug mit Möbeln und Geräten. Die Crew geht – Werkzeug mit –, im Haus bleibt nur, was Sie brauchen.
Praxis: Multi-Stage-Vorlagen für drei Sprachen
Go: von 980 MB auf 10 MB
Go eignet sich besonders, weil statische Binaries möglich sind.
Vollständiges Dockerfile:
# Build-Phase
FROM golang:1.21-alpine AS builder
WORKDIR /app
# go.mod und go.sum zuerst – Cache nutzen
COPY go.mod go.sum ./
RUN go mod download
# Quellcode kopieren und kompilieren
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o myapp .
# Laufzeit-Phase
FROM scratch
# Binärdatei aus builder kopieren
COPY --from=builder /app/myapp /myapp
# CA-Zertifikate (bei HTTPS-Aufrufen)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/myapp"]
Tipps:
FROM scratch: leeres Image, nur Ihre BinärdateiCGO_ENABLED=0: CGO aus, rein statisches Binary- CA-Zertifikate: bei HTTPS-APIs aus Builder-Phase kopieren
- Dependency-Cache: zuerst go.mod/go.sum, dann go mod download – Quelländerungen laden Dependencies nicht neu
Ergebnis: ca. 10 MB. Gegenüber 980 MB: 99 % weniger.
Ist scratch zu extrem (keine Shell, schwer debuggbar), nehmen Sie alpine:
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
Etwas größer (~15 MB), aber docker exec zum Debuggen möglich.
Node.js: von 900 MB auf 120 MB
Bei Node.js etwas komplexer – wegen node_modules.
Vollständiges Dockerfile:
# Build-Phase
FROM node:18-alpine AS builder
WORKDIR /app
# package.json kopieren
COPY package*.json ./
# Alle Dependencies (inkl. devDependencies)
RUN npm ci
# Quellcode kopieren
COPY . .
# Build-Schritt (z. B. TypeScript)
RUN npm run build
# Produktionsphase
FROM node:18-alpine
WORKDIR /app
# Node-Umgebung
ENV NODE_ENV=production
# Nur Produktions-Dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Build-Artefakte kopieren
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Wichtig:
npm ci --only=production: nurdependencies, keinedevDependencies– halbiert oft das Volumennpm cache clean --force: npm-Cache sonst in Layern- Build und Laufzeit getrennt: TypeScript-Kompilierung in builder, Produktions-Image nur JS
Laut Oak Oliver Engineering: typische Express-App unoptimiert ~900 MB, mit Multi-Stage ~120 MB – ca. 87 % Reduktion.
Python: von 300 MB auf 100 MB
Python hat keinen Compile-Schritt, aber große Pakete (numpy, pandas oft hunderte MB).
Vollständiges Dockerfile:
# Build-Phase
FROM python:3.9-slim AS builder
WORKDIR /app
# Dependencies ins User-Verzeichnis
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Produktionsphase
FROM python:3.9-alpine
WORKDIR /app
# Dependencies kopieren
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# Anwendungscode kopieren
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
pip install --user installiert nach /root/.local, das Verzeichnis wandert ins Produktions-Image.
Kernpunkte:
--no-cache-dir: pip-Cache sonst im Image- slim vs. alpine: Build mit
slim(Kompatibilität), Produktion mitalpine(klein) - Virtuelle Umgebung: bei komplexen Dependencies ggf. venv statt
--user
Praxis: FastAPI + SQLAlchemy, Original ~300 MB, Multi-Stage ~100 MB.
Basis-Image: Alpine vs. Distroless vs. Slim
Die Wahl des Laufzeit-Basis-Images ist ein Trade-off.
| Eigenschaft | Alpine | Distroless | Slim |
|---|---|---|---|
| Größe | 3–5 MB | 20–65 MB | 50–100 MB |
| Sicherheit | Mittel | Sehr hoch | Mittel |
| Debug-Aufwand | Niedrig (Shell) | Hoch (keine Shell) | Niedrig (Shell) |
| Kompatibilität | Fallstricke (glibc) | Gut | Gut |
| Einsatz | Go statisch | Hohe Sicherheit | Node.js/Python |
Alpine: klein, aber glibc beachten
Alpine nutzt musl libc statt glibc. Für Go unkritisch (statische Builds), bei Python-/Node-Dependencies manchmal problematisch.
Mein Fall: Python-Projekt mit numpy lief auf Alpine nicht – ImportError: cannot import name 'random'. Ursache: musl/glibc.
Lösungen:
libc6-compatinstallieren:apk add libc6-compat- oder
slimstattalpine
Distroless: Sicherheitsreferenz, Debugging schwierig
Distroless (Google): keine Shell, kein Paketmanager – nur Laufzeit-Notwendiges.
Laut danieldemmel.me eliminiert Distroless viele High-CVE-Risiken – Angreifer haben keine Shell.
Preis: kein docker exec zum Debuggen; Sie verlassen sich auf Logs und Monitoring.
Bei maximaler Sicherheit:
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/myapp /
ENTRYPOINT ["/myapp"]
Slim: ausgewogener Mittelweg
Offizielle -slim-Images (node:18-slim, python:3.9-slim) zwischen Alpine und Voll-Image.
Etwas größer als Alpine, gute Kompatibilität, Shell zum Debuggen. Wer musl/glibc nicht testen will: slim ist pragmatisch.
Empfehlung:
- Go:
scratchoderalpine - Node.js/Python: zuerst
slim, dannalpinetesten - Hohe Sicherheit:
distroless, Debug-Plan vorher
Fallstricke: fünf typische Fehler und Lösungen
Genug Dockerfiles geschrieben – Fallstricke für einen Swimmingpool. Fünf häufige:
Fehler 1: COPY —from=0 – Voll-Copy
Anfänger kopieren die ganze vorherige Phase:
# Falsch
FROM builder
COPY --from=0 /app /app
Damit landen Go-Toolchain, npm-Cache, Temp-Dateien im Image – sofort aufgebläht.
Richtig: nur Nötiges kopieren.
# Richtig
COPY --from=builder /app/myapp /myapp
COPY --from=builder /app/dist /dist
Fehler 2: Cache nicht geleert
apt/apk-Cache bleibt in Layern, auch nach Löschen.
# Falsch (Cache in vorherigem Layer)
RUN apt-get update && apt-get install -y curl
RUN apt-get clean
Richtig: Clean und Install in einem Layer.
# Richtig
RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*
Oder --no-cache:
RUN apk add --no-cache curl
Fehler 3: Alpine-glibc-Kompatibilität
Alpine + musl – manche Python-/Node-Dependencies scheitern.
Typischer Fehler:
ImportError: cannot import name 'random' from 'numpy.random'
Lösung: libc6-compat oder Wechsel auf slim.
Fehler 4: Kein Non-Root-User
Standard: Container läuft als root – Sicherheitsrisiko.
Best Practice: dedizierter User.
RUN adduser -D appuser
USER appuser
Bei Kompromittierung nur normale User-Rechte.
Fehler 5: .dockerignore vergessen
.dockerignore ist die Subtraktionsliste fürs Dockerfile. Ohne sie kopiert COPY . . alles – .git, node_modules, Tests …
.dockerignore anlegen:
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
*.md
.env
Kleinerer Build-Kontext, schnellere Builds.
Fazit
Multi-Stage-Build ist der praktischste Weg, Docker-Images zu verschlanken.
Kern in einem Satz: Build-Umgebung behält Kompilier-Tools; Laufzeit-Image nur die App.
Daten im Überblick:
- Go: 980 MB → 10 MB (99 % weniger)
- Node.js: 900 MB → 120 MB (87 % weniger)
- Python: 300 MB → 100 MB (67 % weniger)
Noch kein Multi-Stage-Build genutzt? Nehmen Sie ein Projekt, passen Sie das Dockerfile an die Vorlagen an, vergleichen Sie mit docker images.
Sie werden überrascht sein – zumindest timeout der CI/CD-Push nicht mehr.
Docker-Image-Optimierung mit Multi-Stage-Builds
Vollständiger Ablauf, um Docker-Images von aufgebläht auf minimal zu reduzieren
⏱️ Estimated time: 30 min
- 1
Step 1: Aktuelle Image-Zusammensetzung analysieren
Mit `docker history` die Größe der einzelnen Layer prüfen:
```bash
docker history your-image:tag
```
Die größten Layer sind meist:
• das Basis-Image selbst
• Build-Tools und Kompilier-Abhängigkeiten
• Paketmanager-Cache - 2
Step 2: Multi-Stage-Dockerfile schreiben
Dockerfile mit Build- und Laufzeitphase anlegen:
```dockerfile
# Build-Phase
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
# Laufzeit-Phase
FROM alpine:3.18
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
```
Wichtig:
• Phasen mit AS benennen
• COPY --from=builder nur die nötigen Dateien kopieren - 3
Step 3: Image bauen und Größe vergleichen
Neues Image bauen und Volumen vergleichen:
```bash
docker build -t myapp:optimized .
docker images | grep myapp
```
Größenunterschied vor und nach der Optimierung messen. - 4
Step 4: Anwendungsfunktion verifizieren
Container starten und testen:
```bash
docker run -d -p 8080:8080 myapp:optimized
curl http://localhost:8080/health
```
Funktionalität und fehlende Abhängigkeiten prüfen. - 5
Step 5: In Produktion deployen
CI/CD auf das neue Image umstellen:
• Push ins Image-Registry
• Kubernetes Deployment oder docker-compose.yml aktualisieren
• Deployment-Erfolg verifizieren
FAQ
Verlangsamt Multi-Stage-Build den Build?
Alpine oder Distroless – was wählen?
Für welche Sprachen eignet sich Multi-Stage-Build?
Wie Konfigurationsdateien in Multi-Stage-Builds?
Wie viel Image-Volumen spart Multi-Stage-Build?
Was bei FROM scratch beachten?
7 Min. Lesezeit · Veröffentlicht am: 19. Apr. 2026 · Aktualisiert am: 14. Juli 2026
Docker Praxisleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Docker Multi-Stage-Build in der Praxis: Go-/Java-/Rust-Images von GB auf MB schlanken
Multi-Stage-Builds im Detail: Go -98 %, Java -86 %, Rust -99,4 %. Mit vollständigen Dockerfiles und Praxis-Pitfalls aus echten Projekten.
Teil 5 von 38
Nächster
Docker-Build beschleunigen: Cache nutzen für 10x schnellere Builds – Praxisleitfaden
Layer-Cache, .dockerignore und Dockerfile-Optimierung: Build-Zeit von 10 Minuten auf 30 Sekunden. Mit Codebeispielen und BuildKit-Cache-Mounts für Fortgeschrittene.
Teil 7 von 38
Ähnliche Beiträge
Docker vs. virtuelle Maschine: Performance-Unterschiede und Szenario-Auswahl in 5 Minuten

Docker vs. virtuelle Maschine: Performance-Unterschiede und Szenario-Auswahl in 5 Minuten
Docker-Installation ohne Fallstricke (2025): Vollständige Lösungen von permission denied bis zum erfolgreichen Start


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen