Design wechseln

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

Easton editorial illustration: tradeoff balance table

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

  1. Basis-Image zu groß: ubuntu:20.04 allein ~77 MB, mit Go-Toolchain über 900 MB
  2. Build-Tools bleiben drin: gcc, make, git – in Produktion unnötig
  3. Cache nicht geleert: apt/apk-Cache bleibt in den Layern
  4. 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 benennen
  • COPY --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:

  1. FROM scratch: leeres Image, nur Ihre Binärdatei
  2. CGO_ENABLED=0: CGO aus, rein statisches Binary
  3. CA-Zertifikate: bei HTTPS-APIs aus Builder-Phase kopieren
  4. 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:

  1. npm ci --only=production: nur dependencies, keine devDependencies – halbiert oft das Volumen
  2. npm cache clean --force: npm-Cache sonst in Layern
  3. 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:

  1. --no-cache-dir: pip-Cache sonst im Image
  2. slim vs. alpine: Build mit slim (Kompatibilität), Produktion mit alpine (klein)
  3. 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.

EigenschaftAlpineDistrolessSlim
Größe3–5 MB20–65 MB50–100 MB
SicherheitMittelSehr hochMittel
Debug-AufwandNiedrig (Shell)Hoch (keine Shell)Niedrig (Shell)
KompatibilitätFallstricke (glibc)GutGut
EinsatzGo statischHohe SicherheitNode.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-compat installieren: apk add libc6-compat
  • oder slim statt alpine

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: scratch oder alpine
  • Node.js/Python: zuerst slim, dann alpine testen
  • 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. 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. 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. 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. 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. 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?
Multi-Stage-Builds dauern etwas länger (zwei Phasen), aber das finale Image ist deutlich kleiner – Deployment und Transfer werden schneller. In CI/CD-Pipelines sinkt die Gesamtzeit meist.
Alpine oder Distroless – was wählen?
Go mit statischer Kompilierung: Alpine oder scratch. Node.js/Python: zuerst slim für Kompatibilität, dann Alpine testen. Bei hohen Sicherheitsanforderungen Distroless – Logging und Monitoring vorher planen.
Für welche Sprachen eignet sich Multi-Stage-Build?
Praktisch für alle Sprachen. Am stärksten: Go (bis ~10 MB), Node.js (80 %+ Reduktion), Python (60 %+), Rust, Java und andere mit Build- oder Abhängigkeitsmanagement.
Wie Konfigurationsdateien in Multi-Stage-Builds?
Konfiguration meist per Volume mounten, nicht ins Image packen – Docker Volume oder Kubernetes ConfigMap. Falls nötig: in der Laufzeitphase per COPY einbinden.
Wie viel Image-Volumen spart Multi-Stage-Build?
Abhängig von Sprache und App. Go oft 90–99 % (1 GB → 10 MB); Node.js 70–90 %; Python 50–70 %. Entscheidend: nur laufzeitnotwendige Dateien behalten.
Was bei FROM scratch beachten?
scratch ist leer – kein Shell, kein Paketmanager, keine CA-Zertifikate. Bei HTTPS-Aufrufen /etc/ssl/certs/ca-certificates.crt aus der Builder-Phase kopieren. Debugging schwierig – zuerst mit alpine verifizieren.

7 Min. Lesezeit · Veröffentlicht am: 19. Apr. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog