Docker Multi-Stage-Build in der Praxis: Go-/Java-/Rust-Images von GB auf MB schlanken

Ein 650-MB-Image als Weckruf
Freitagnachmittag, ich starre auf das K8s-Dashboard: Ein Pod zieht seit fünf Minuten ein Image. Eine simple Spring-Boot-App – Docker-Image: 650 MB. Ein Praktikant fragt: „Warum ist das so groß?” Ich merke: Ich habe mir die Frage selten gestellt.
Abends recherchiert, morgens dasselbe Image: 89 MB. Deployment von 5 Minuten auf unter 1 Minute. Der Praktikant schaut mich anders an.
Nur ein paar Zeilen im Dockerfile geändert. Die Technik heißt Multi-Stage-Build.
Anfangs wirkte das harmlos. Unsere Go-Images: 295 MB, Java oft 500 MB+ – man gewöhnt sich daran. Bis jemand ein Rust-Image von 2 GB auf 11 MB drückte. Dann wurde klar: Wir packen falsch.
Kern des Problems: Kompilierte Sprachen brauchen Compiler und Build-Tools – zur Laufzeit nicht. Klassische Dockerfiles packen Maven, Gradle und Go-Compiler mit. Wie beim Umzug Bohrmaschine und Zement mitnehmen.
Heute zeige ich Go-, Java- und Rust-Beispiele:
- Go: 295 MB → 6,47 MB (-98 %)
- Java Spring Boot: 650 MB → 89 MB (-86 %)
- Rust: 2 GB → 11,2 MB (-99,4 %)
Nur eine Sprache relevant? Direkt zum passenden Abschnitt springen – jedes Beispiel ist vollständig und kopierbar.
Warum ist Ihr Image so aufgebläht?
Vergleichstabelle aus echten Tests:
| Sprache | Klassischer Single-Stage-Build | Multi-Stage-Build | Reduktion |
|---|---|---|---|
| Go-App | 295 MB | 6,47 MB | 98 % |
| Java Spring Boot | 650 MB | 89 MB | 86 % |
| Rust-App | 2,1 GB | 11,2 MB | 99,4 % |
Beim Rust-Wert war ich sprachlos: 2 GB auf 11 MB – das wirkt wie Magie.
Die Analyse zeigt: Kompilierte Sprachen brauchen Compiler. Go braucht den Go-Compiler, Java Maven oder Gradle, Rust Cargo. Diese Tools sind groß:
- Go-Compiler: ca. 300 MB
- Maven + OpenJDK: ca. 500 MB
- Rust-Toolchain: ca. 1,5 GB
Klassisches Dockerfile:
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
Sieht harmlos aus – ist es nicht. Das gesamte golang:1.21-Image (295 MB) landet im finalen Image: Compiler, Build-Tools, Debug-Tools. Nach dem Build bleibt alles drin.
Wie ein Rohbau: Die Handwerker renovieren, danach bleiben Zement, Bohrmaschine und Werkzeugkisten im Zimmer – und Sie ziehen ein.
Zur Laufzeit brauchen Sie nur das kompilierte Binary. Go-Binaries: wenige MB. Java-JARs: einige Dutzend MB. Rust-Binaries: ebenfalls klein. Die restlichen hunderte MB sind Compiler und Basis-Image.
Die Folgen gehen über Speicher hinaus:
- Langsames Pullen: In CI/CD muss jedes Deployment das Image ziehen. 650 MB bei schlechtem Netz – frustrierend
- Sicherheitsrisiko: Compiler, Quellcode und Build-Tools in Produktion – Werkzeugkiste für Angreifer
- Cache-Verschwendung: Eine Codezeile ändert sich, das ganze Image wird neu gebaut – Compiler und Code sind gekoppelt
Einmal bei Alibaba Cloud: Fünf Personen deployen gleichzeitig, jedes Image 500 MB+ – das interne Netz war voll. Seitdem: Image-Optimierung.
Multi-Stage-Build: Ein Dockerfile, zwei Umgebungen
Multi-Stage-Builds gibt es seit Docker 17.05. Kernidee: Mehrere Phasen in einem Dockerfile – frühere kompilieren, spätere laufen, nur nötige Artefakte weitergeben.
Einfach gesagt: Phase 1 renoviert, Phase 2 bekommt nur das fertige Haus – Zement und Bohrmaschine bleiben in Phase 1.
Minimalbeispiel:
# Phase 1: Build
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Phase 2: Runtime
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Die Schlüsselpunkte:
- Erstes FROM mit
AS builder– Phase benennen - Zweites FROM startet neue Phase mit leichtem Alpine-Image
- COPY —from=builder kopiert nur das Binary – der Rest wird verworfen
Phase 1: golang:1.21 mit 295 MB. Finales Image: Alpine (~5 MB) plus Binary – insgesamt wenige MB.
Der Aha-Moment: Nur das Ergebnis, nicht den Prozess. Compiler, Quellcode und Zwischendateien sind Prozess – das Binary ist Ergebnis. Docker verwirft Phase 1, behält Phase 2.
Debugging in Phase 1?
docker build --target builder -t myapp:debug .
--target builder baut nur bis zur builder-Phase – ideal zum Debuggen.
Mehrere Phasen sind möglich:
FROM node:18 AS frontend-builder
# Frontend-Build
FROM golang:1.21 AS backend-builder
# Backend-Build
FROM nginx:alpine
# Frontend- und Backend-Artefakte zusammenführen
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
COPY --from=backend-builder /app/api /usr/local/bin/api
Jede Phase erledigt ihre Aufgabe, die Runtime-Phase fasst zusammen. Die Dockerfile-Logik wird klarer.
Heute nutze ich bei kompilierten Sprachen standardmäßig Multi-Stage-Builds – Gewohnheit.
Go-Apps maximal schlank
Go ist ideal für Image-Optimierung: Statisch gelinkte Binaries, keine Systembibliotheken nötig – direkt in ein leeres Image.
Klassisch (nicht nachmachen):
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
Build:
docker build -t myapp:old .
docker images myapp:old
# REPOSITORY TAG IMAGE ID SIZE
# myapp old abc123def456 295MB
295 MB für einen einfachen HTTP-Service.
Multi-Stage-Version:
# Build-Phase
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Abhängigkeiten 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 -ldflags="-w -s" -o myapp .
# Runtime-Phase
FROM scratch
WORKDIR /app
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]
Erneut bauen:
docker build -t myapp:new .
docker images myapp:new
# REPOSITORY TAG IMAGE ID SIZE
# myapp new def456ghi789 6.47MB
6,47 MB – von 295 MB, minus 98 %.
Schlüsselpunkte:
1. CGO_ENABLED=0
Deaktiviert CGO – rein statisches Binary. Bei C-Bibliotheken (z. B. SQLite) funktioniert das nicht; dann anderes Basis-Image.
2. -ldflags=“-w -s”
Compiler-Optimierung:
-w: Debug-Infos entfernen-s: Symboltabelle entfernen
20–30 % kleineres Binary. In Produktion meist unnötig.
3. FROM scratch
scratch ist das leere Docker-Image – 0 Byte. Go-Static-Binaries laufen direkt darin.
4. Abhängigkeits-Cache
Zuerst go.mod und go.sum kopieren, dann go mod download. Solange diese Dateien gleich bleiben, wird die Abhängigkeits-Layer gecacht. Code-Änderungen triggern keinen erneuten Download.
Mit diesem Trick: Rebuild nach Code-Änderung von 2 Minuten auf 15 Sekunden.
Fortgeschritten: Zeitzone und CA-Zertifikate
scratch enthält weder Zeitzonendaten noch CA-Zertifikate. Für HTTPS oder Zeitzonen aus der builder-Phase kopieren:
FROM scratch
WORKDIR /app
# Zeitzonendaten
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# CA-Zertifikate
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/myapp .
ENV TZ=Asia/Shanghai
CMD ["./myapp"]
Oder gcr.io/distroless/static-debian11 statt scratch – ca. 2 MB, mit Zeitzonen und CA-Zertifikaten:
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/myapp /app/myapp
CMD ["/app/myapp"]
Ich nutze meist distroless – weniger Aufwand.
Java/Spring Boot elegant schlanken
Java-Optimierung ist komplexer: JRE nötig, kein scratch. Mit der richtigen Methode lässt sich das Volumen deutlich drücken.
Klassisch (viele Java-Projekte):
FROM maven:3.8-openjdk-17
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
CMD ["java", "-jar", "target/myapp.jar"]
Ergebnis: 650 MB. Maven-Image allein ~500 MB, plus JAR und Caches.
Multi-Stage-Version:
# Build-Phase
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /app
# pom.xml zuerst, Abhängigkeiten cachen
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Quellcode und Paketierung
COPY src ./src
RUN mvn package -DskipTests
# Runtime-Phase
FROM openjdk:17-jre-slim
WORKDIR /app
# Nur JAR kopieren
COPY --from=builder /app/target/*.jar app.jar
# JVM-Tuning
ENV JAVA_OPTS="-Xms128m -Xmx512m -XX:+UseContainerSupport"
EXPOSE 8080
CMD java $JAVA_OPTS -jar app.jar
Ergebnis:
docker images myapp:new
# REPOSITORY TAG IMAGE ID SIZE
# myapp new xyz789abc012 89MB
650 MB → 89 MB, minus 86 %.
Schlüsselpunkte:
1. JDK vs. JRE
Build: openjdk-17 (mit Compiler). Runtime: openjdk-17-jre-slim (nur Laufzeit). JRE ist deutlich kleiner:
- OpenJDK 17: ca. 500 MB
- OpenJDK 17 JRE: ca. 200 MB
- OpenJDK 17 JRE Slim: ca. 80 MB
2. Maven-Abhängigkeits-Cache
Zuerst pom.xml, dann mvn dependency:go-offline. Solange pom.xml gleich bleibt, wird die Layer gecacht. Code-Änderungen triggern keinen erneuten Download.
Ohne diesen Trick: Jede Codezeile → Dependencies neu laden. Maven-Repos im Ausland – oft 10+ Minuten Wartezeit. Jetzt: Sekunden.
3. JVM-Parameter
-XX:+UseContainerSupport: JVM erkennt Container-Memory-Limits. Ohne das: Heap nach Host-RAM – OOM-Gefahr.
-Xms128m -Xmx512m: Heap-Bereich. An die App anpassen, nicht blind 1 GB setzen.
Gradle-Variante
Mit Gradle leicht angepasst:
FROM gradle:8.5-jdk17 AS builder
WORKDIR /app
# Gradle-Konfiguration
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
# Abhängigkeiten
RUN gradle dependencies --no-daemon
# Quellcode und Build
COPY src ./src
RUN gradle bootJar --no-daemon
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
ENV JAVA_OPTS="-Xms128m -Xmx512m -XX:+UseContainerSupport"
CMD java $JAVA_OPTS -jar app.jar
Pitfall: Spring Boot Layered JAR
Spring Boot 2.3+ unterstützt JAR-Layering – Dependencies und Business-Code getrennt, besserer Cache:
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
RUN java -Djarmode=layertools -jar target/*.jar extract
FROM openjdk:17-jre-slim
WORKDIR /app
# Layer-Reihenfolge: Dependencies ändern sich selten
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
CMD ["java", "org.springframework.boot.loader.JarLauncher"]
Code-Änderungen invalidieren die Dependency-Layer nicht – schnellerer Build. Ich nutze das selten; der einfache Ansatz reicht meist.
Rust: Minimales Deployment
Rust liefert den stärksten Effekt: Toolchain 1,5 GB+, Binary winzig. 2,1 GB → 11,2 MB – beeindruckend.
Klassisch:
FROM rust:1.75
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/myapp"]
Ergebnis: 2,1 GB. rustc, cargo, Dependencies – alles im Image.
Multi-Stage-Version:
# Build-Phase
FROM rust:1.75 AS builder
WORKDIR /app
# Abhängigkeiten zuerst kompilieren (Cache)
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# Echten Code kompilieren
COPY src ./src
RUN touch src/main.rs # Timestamp aktualisieren, Rebuild triggern
RUN cargo build --release
# Runtime-Phase
FROM gcr.io/distroless/cc-debian11
WORKDIR /app
COPY --from=builder /app/target/release/myapp .
CMD ["./myapp"]
Ergebnis:
docker images myapp:new
# REPOSITORY TAG IMAGE ID SIZE
# myapp new rst345uvw678 11.2MB
11,2 MB – minus 99,4 %.
Rust-spezifischer Cache
Dependency-Builds dauern oft 10+ Minuten. Trick: Fake-main.rs erzeugen, Dependencies kompilieren, löschen, echten Code kopieren und neu bauen.
Solange Cargo.toml und Cargo.lock gleich bleiben, wird die Dependency-Layer gecacht. Code-Änderungen kompilieren nur den App-Code.
In einem mittelgroßen Projekt: Rebuild von 15 Minuten auf 2 Minuten.
Static vs. Dynamic Linking
Rust-Binaries können glibc brauchen. Bei reinem Rust ohne C-Bibliotheken: musl für statisches Binary, dann scratch:
FROM rust:1.75 AS builder
WORKDIR /app
# musl-Toolchain
RUN rustup target add x86_64-unknown-linux-musl
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN rm -rf src
COPY src ./src
RUN touch src/main.rs
RUN cargo build --release --target x86_64-unknown-linux-musl
# Runtime-Phase
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
CMD ["/myapp"]
Noch etwas kleiner – vielleicht 5–8 MB.
Ich nutze meist gcr.io/distroless/cc – bessere Kompatibilität, nur wenige MB mehr.
Pitfall: Cargo-Cache-Verzeichnis
Manche Tutorials cachen /usr/local/cargo. Nicht tun. Viele Zwischenartefakte – riesige Cache-Layer, langsamer statt schneller. Mein Fehler: 800-MB-Cache-Layer, lange Wartezeiten.
Build schneller und stabiler machen
Grundlagen erklärt – jetzt Tipps für Geschwindigkeit und Stabilität.
1. .dockerignore
Oft übersehen, aber wichtig. Wie .gitignore: Dateien vom Build-Context ausschließen.
Ohne .dockerignore: Docker packt das ganze Projekt (node_modules, .git, target) und sendet es an den Daemon. Hunderte MB, allein der Context-Upload dauert.
Jedes Projekt bekommt .dockerignore:
# Versionskontrolle
.git
.gitignore
# Abhängigkeitsverzeichnisse
node_modules
target
dist
build
# IDE
.vscode
.idea
*.swp
# Tests und Docs
**/*_test.go
**/*_test.rs
*.md
docs/
# Umgebungsvariablen und Secrets
.env
.env.local
*.key
*.pem
Build 3–5× schneller – in CI/CD besonders spürbar.
2. Multi-Stage-Build debuggen
--target im Detail. Build scheitert in builder-Phase?
# Nur bis builder bauen
docker build --target builder -t myapp:debug .
# In Image einsteigen
docker run -it myapp:debug sh
# Build-Befehle manuell ausführen
Praktisch: Go-Build schlug fehl – mit --target builder und manuellem go build war klar: Go-Version inkompatibel mit Dependency.
3. Phasen sinnvoll benennen
Aussagekräftige Namen statt stage1, stage2:
# Schlecht
FROM golang:1.21 AS stage1
FROM node:18 AS stage2
FROM nginx AS stage3
# Gut
FROM golang:1.21 AS backend-builder
FROM node:18 AS frontend-builder
FROM nginx AS runtime
Monate später danken Sie sich dafür.
4. BuildKit parallel bauen
BuildKit: Parallele Builds, besserer Cache. Aktivieren:
export DOCKER_BUILDKIT=1
docker build .
Oder einmalig:
DOCKER_BUILDKIT=1 docker build .
Vorteile:
- Unabhängige Phasen parallel
- Intelligenteres Caching
- Klarere Build-Ausgabe
Ich nutze BuildKit standardmäßig, auch in CI/CD mit DOCKER_BUILDKIT=1.
5. Basis-Image-Version fixieren
Nicht FROM golang:latest – Produktion braucht feste Versionen:
# Schlecht – unvorhersehbar
FROM golang:latest
# Gut – explizite Version
FROM golang:1.21.5-alpine3.18
# Besser – SHA256
FROM golang@sha256:abc123...
Einmal Deployment-Fehler: golang:latest hatte sich geändert, neue Version inkompatibel. Seitdem: feste Versionen.
6. COPY-Reihenfolge für Cache
Selten geänderte Dateien zuerst, oft geänderte zuletzt:
# Gute Reihenfolge
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 1. Abhängigkeiten (selten geändert)
COPY go.mod go.sum ./
RUN go mod download
# 2. Quellcode (oft geändert)
COPY . .
RUN go build -o myapp
# Schlechte Reihenfolge
COPY . . # Alles auf einmal
RUN go mod download && go build -o myapp
Erste Variante: Code-Änderung triggert keinen erneuten Dependency-Download. Zweite: Jede Änderung lädt Dependencies neu.
Kernprinzip des Cache-Tunings – in allen Beispielen oben.
Basis-Image-Auswahl
Alpine, Slim, Distroless, Scratch – was wählen? Vergleich aus der Praxis:
| Image-Typ | Größe | Inhalt | Vorteile | Nachteile | Einsatz |
|---|---|---|---|---|---|
| scratch | 0 MB | Leer | Minimal, kleinste Angriffsfläche | Kein Shell, kein Debug, keine CA-Zerts | Go/Rust static |
| distroless | 2–20 MB | Runtime-Libs, CA-Zerts | Kein Shell, sicher, klein | Schwer debuggbar | Go, Java, Rust, Node.js |
| alpine | 5–40 MB | musl libc, Paketmanager | Klein, Shell vorhanden | musl-Kompatibilität, DNS-Probleme | Apps ohne glibc |
| slim | 70–120 MB | Debian/Ubuntu minimal | glibc, kompatibel | Etwas größer | C-Library-Abhängigkeiten |
| Vollständig | 200 MB+ | Komplettes System | Alle Tools | Groß, unsicher | Nicht für Produktion |
Meine Strategie:
Go
- Erste Wahl:
gcr.io/distroless/static-debian11(mit CA-Zerts) - Alternative:
scratch(CA und Zeitzone manuell kopieren) - Mit CGO:
gcr.io/distroless/base-debian11oderalpine
Java
- Erste Wahl:
openjdk:17-jre-slim(vollständiges JRE, kompatibel) - Fortgeschritten:
gcr.io/distroless/java17-debian11(kleiner, schwerer debuggbar) - Vermeiden:
openjdk:17-alpine(JVM-Bugs auf Alpine)
Rust
- Erste Wahl:
gcr.io/distroless/cc-debian11(C-Runtime) - Static:
scratch - Vermeiden: volles
rust-Image in Produktion
Alpine-Pitfalls
Alpine ist beliebt, aber:
- musl libc: Nicht glibc – Java/Python zeigen manchmal seltsame Bugs
- DNS: Go auf Alpine manchmal DNS-Timeouts, Spezialkonfiguration nötig
- Zeitzone: Keine Zeitzonendaten standardmäßig –
tzdatainstallieren
Unbekannt mit Alpine? -slim ist sicherer. Etwas größer, weniger Überraschungen.
Distroless
Google-Minimal-Images: kein Shell, kein Paketmanager – nur Runtime-Essentials.
Vorteile:
- Minimale Angriffsfläche (kein Shell → schwer Befehle ausführen)
- Klein
- Offiziell gepflegt, Security-Patches
Nachteile:
- Debugging schwierig, kein
docker exec -it - Alles in der Build-Phase vorbereiten
Produktion: meist Distroless. Debug: --target builder. Runtime: Distroless.
Entscheidungsbaum
Welche Sprache?
├─ Go
│ ├─ Reines Go → distroless/static oder scratch
│ └─ Mit CGO → distroless/base oder alpine
├─ Java
│ ├─ Stabilität → openjdk:jre-slim
│ └─ Größe → distroless/java
├─ Rust
│ ├─ Reines Rust → distroless/cc oder scratch
│ └─ Mit C-Libs → distroless/cc
└─ Sonstiges
└─ Erst slim, bei Problemen wechseln
Aus echten Projekten – deckt ca. 90 % der Fälle ab.
Das Wesentliche in drei Punkten
Multi-Stage-Builds sind einfach:
- Build-Phase: Vollständiges Image kompiliert
- Runtime-Phase: Schlankes Image führt aus
- COPY —from: Nur nötige Artefakte weitergeben
Effekt: 70–90 % kleiner, 2–3× schneller mit Cache, deutlich sicherer.
Neue Projekte: Multi-Stage-Dockerfile als Erstes – Go, Java, Rust. Gewohnheit.
Handlungsempfehlungen:
Heute umsetzbar:
- Bestehendes Projekt mit Multi-Stage testen
docker imagesvorher/nachher vergleichen- Bei Erfolg auf andere Projekte ausrollen
Vertiefen:
- Docker-Dokumentation Best Practices
divefür Layer-Analyse (docker run --rm -it wagoodman/dive:latest your-image)- BuildKit-Features
Langfristig:
- Image-Cache in CI/CD
- Basis-Images regelmäßig aktualisieren
- Security-Scan mit Trivy o. Ä.
Zum Schluss: Technisch ist Image-Optimierung nicht schwer – schwer ist das Bewusstsein. Viele Teams lassen Images jahrelang wachsen, bis Deployments unerträglich werden.
Nicht warten. Multi-Stage heute ausprobieren – Images können so klein sein, Builds so schnell.
In den Kommentaren: Wie groß sind Ihre Images? Was hat bei der Optimierung geholfen – und wo sind Sie reingelaufen?
Vollständiger Optimierungs-Workflow für Docker Multi-Stage-Builds
Go-/Java-/Rust-Images von GB auf MB schlanken – mit Dockerfiles und Praxis-Pitfalls
⏱️ Estimated time: 1 hr
- 1
Step 1: Multi-Stage-Prinzip verstehen
Multi-Stage-Prinzip:
• Kompilierte Sprachen brauchen Compiler und Build-Tools, um Quellcode in Binaries zu übersetzen
• Zur Laufzeit sind diese Tools nicht nötig
• Klassische Dockerfiles packen Maven, Gradle und Go-Compiler mit
• Wie beim Umzug Bohrmaschine und Zement mitnehmen – völlig unnötig
Kern des Problems:
• Kompilierte Sprachen brauchen Compiler und Build-Tools
• Zur Laufzeit sind diese nicht nötig
Multi-Stage-Schritte:
• Phase 1 (Build): Vollständiges Build-Image mit Compiler und Tools, Quellcode kompilieren
• Phase 2 (Runtime): Minimales Runtime-Image, nur das Binary aus Phase 1 kopieren
• Finales Image enthält nur Runtime und ausführbare Datei - 2
Step 2: Go Multi-Stage-Build in der Praxis
Go Multi-Stage-Build:
Phase 1 (Build):
• FROM golang:1.21 AS builder
• WORKDIR /app
• COPY go.mod go.sum ./
• RUN go mod download
• COPY . .
• RUN go build -o app
Phase 2 (Runtime):
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/app .
• CMD ["./app"]
Optimierung:
• Nur das kompilierte Binary kopieren
• Image von 295 MB auf 6,47 MB (-98 %)
• Deployment von 5 Minuten auf unter 1 Minute
• Deutlich kleinere Images - 3
Step 3: Java- und Rust Multi-Stage-Builds in der Praxis
Java Multi-Stage-Build:
Phase 1 (Build):
• FROM maven:3.9 AS builder
• WORKDIR /app
• COPY pom.xml .
• RUN mvn dependency:go-offline
• COPY src ./src
• RUN mvn clean package -DskipTests
Phase 2 (Runtime):
• FROM eclipse-temurin:17-jre-alpine
• WORKDIR /app
• COPY --from=builder /app/target/app.jar app.jar
• CMD ["java", "-jar", "app.jar"]
Optimierung: Image von 650 MB auf 89 MB (-86 %)
Rust Multi-Stage-Build:
Phase 1 (Build):
• FROM rust:1.75 AS builder
• WORKDIR /app
• COPY Cargo.toml Cargo.lock .
• RUN cargo fetch
• COPY src ./src
• RUN cargo build --release
Phase 2 (Runtime):
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/target/release/app .
• CMD ["./app"]
Optimierung: Image von 2 GB auf 11 MB (-99,4 %) - 4
Step 4: Best Practices und langfristige Optimierung
Best Practices:
1. Passendes Runtime-Image wählen
• Go: distroless/static oder scratch
• Java: openjdk:jre-slim oder distroless/java
• Rust: distroless/cc oder scratch
2. .dockerignore für unnötige Dateien
3. Build-Cache nutzen (Abhängigkeiten zuerst, dann Quellcode)
4. Basis-Images regelmäßig aktualisieren
Langfristig:
• Image-Cache in CI/CD-Pipeline
• Basis-Images regelmäßig aktualisieren
• Security-Scan mit Trivy o. Ä.
Technisch ist Image-Optimierung nicht schwer – schwer ist das Bewusstsein. Viele Teams lassen Images jahrelang wachsen, bis Deployments unerträglich langsam werden.
FAQ
Was ist Docker Multi-Stage-Build? Warum braucht man es?
Kern: Compiler und Build-Tools sind nur für den Build nötig, nicht zur Laufzeit.
Schritte:
• Phase 1: Vollständiges Build-Image kompiliert Quellcode
• Phase 2: Minimales Runtime-Image, nur Binary kopieren
• Finales Image: Runtime + ausführbare Datei
Wie stark optimiert Multi-Stage-Build?
• Go: 295 MB → 6,47 MB (-98 %)
• Java Spring Boot: 650 MB → 89 MB (-86 %)
• Rust: 2 GB → 11 MB (-99,4 %)
• Deployment: 5 Minuten → unter 1 Minute
Beispiel: Spring-Boot-App als Docker-Image 650 MB, mit Multi-Stage nur 89 MB – Deployment von 5 Minuten auf unter 1 Minute.
Wie setzt man Go Multi-Stage-Build um?
Phase 1 mit golang:1.21:
• FROM golang:1.21 AS builder
• WORKDIR /app
• COPY go.mod go.sum ./
• RUN go mod download
• COPY . .
• RUN go build -o app
Phase 2 mit alpine:latest:
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/app .
• CMD ["./app"]
Nur das Binary kopieren – von 295 MB auf 6,47 MB (-98 %). Deployment von 5 Minuten auf unter 1 Minute.
Wie setzt man Java- und Rust Multi-Stage-Build um?
Phase 1 mit maven:3.9:
• FROM maven:3.9 AS builder
• WORKDIR /app
• COPY pom.xml .
• RUN mvn dependency:go-offline
• COPY src ./src
• RUN mvn clean package -DskipTests
Phase 2 mit eclipse-temurin:17-jre-alpine:
• FROM eclipse-temurin:17-jre-alpine
• WORKDIR /app
• COPY --from=builder /app/target/app.jar app.jar
• CMD ["java", "-jar", "app.jar"]
650 MB → 89 MB (-86 %)
Rust Multi-Stage-Build:
Phase 1 mit rust:1.75:
• FROM rust:1.75 AS builder
• WORKDIR /app
• COPY Cargo.toml Cargo.lock .
• RUN cargo fetch
• COPY src ./src
• RUN cargo build --release
Phase 2 mit alpine:latest:
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/target/release/app .
• CMD ["./app"]
2 GB → 11 MB (-99,4 %)
Was sind Best Practices für Multi-Stage-Builds?
1) Passendes Runtime-Image:
• Go: distroless/static oder scratch
• Java: openjdk:jre-slim oder distroless/java
• Rust: distroless/cc oder scratch
2) .dockerignore nutzen
3) Build-Cache (Abhängigkeiten zuerst, dann Quellcode)
4) Basis-Images regelmäßig aktualisieren
Langfristig:
• Image-Cache in CI/CD
• Basis-Images aktualisieren
• Security-Scan mit Trivy
Technisch nicht schwer – schwer ist das Bewusstsein. Viele Teams optimieren erst, wenn Deployments unerträglich langsam werden.
11 Min. Lesezeit · Veröffentlicht am: 17. Dez. 2025 · 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
Dockerfile-Optimierung in der Praxis: 5 Tipps, die Image-Größe um 80 % reduzieren
Docker-Images von mehreren GB? Mit Alpine-Basis-Image, zusammengeführten RUN-Anweisungen, Multi-Stage-Build, .dockerignore und Cache-Bereinigung schrumpft ein Image von 1,2 GB auf 180 MB – minus 85 %. Inklusive vollständigem Node.js-Optimierungsfall und Messdaten.
Teil 4 von 38
Nächster
Docker Multi-Stage-Builds in der Praxis: Produktions-Images von 1 GB auf 10 MB schrumpfen
Multi-Stage-Builds für Docker meistern und Produktions-Images von 1 GB auf 10 MB reduzieren. Mit Go-, Node.js- und Python-Vorlagen, Alpine vs. Distroless und fünf typischen Fallstricken.
Teil 6 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