Docker Multistage-Builds
Was dieser Artikel über Multistage-Builds behandelt
- Was Multistage-Builds sind.
- Wie man Build-Tools vom Runtime-Image trennt.
- Praktische Dockerfile-Beispiele.
- Vorteile für Sicherheit und Image-Grösse.
- Tipps für produktive Images.
Einleitung: Docker Multistage-Builds
Ein Docker-Image sollte möglichst klein und frei von unnötigen Werkzeugen sein. Wer alles in ein einziges Image packt, schleppt Compiler, Entwicklungsbibliotheken und Testdateien mit. Multistage-Builds lösen das Problem, indem sie den Build-Prozess von der tatsächlichen Laufzeitumgebung trennen. Man baut die Anwendung in einem Image mit allen Werkzeugen und kopiert am Ende nur die benötigten Artefakte in ein kleines Runtime-Image.
Dieser Artikel zeigt, wie Multistage-Builds funktionieren und was bei der Umsetzung zu beachten ist.
Wichtige Begriffe
- Multistage-Build: Dockerfile mit mehreren
FROM-Anweisungen. - Build-Stage: Image, in dem kompiliert und vorbereitet wird.
- Runtime-Stage: Image, das schlussendlich ausgeführt wird.
- Distroless: Container-Image ohne Shell oder Paketmanager.
- Alpine: Kleines Linux-Image.
- Scratch: Leeres Basis-Image.
- Artefakt: Ergebnis eines Builds.
- Layer: Schicht eines Images.
Wann Multistage-Builds sinnvoll sind
- Anwendungen müssen kompiliert werden.
- Build-Abhängigkeiten dürfen nicht im fertigen Image landen.
- Image-Grösse reduziert werden soll.
- Sicherheitsfläche verkleinert werden soll.
- Mehrere Schritte nötig sind, beispielsweise Test und Build.
Einfaches Beispiel: Python
# Build-Stage
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .
# Runtime-Stage
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Einfaches Beispiel: Go
# Build-Stage
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o main .
# Runtime-Stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /main
CMD ["/main"]
Dieses Image hat weder Shell noch Paketmanager.
Einfaches Beispiel: Node.js
# Build-Stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime-Stage
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
ENV NODE_ENV=production
RUN npm ci --only=production
CMD ["node", "dist/main.js"]
Alpine als Runtime
Alpine ist klein, aber musl-basiert. Für manche Anwendungen sind zusätzliche Bibliotheken nötig:
FROM alpine:latest
RUN apk add --no-cache libstdc++
COPY --from=builder /app/main /main
CMD ["/main"]
Distroless-Images
Distroless-Images enthalten nur Laufzeitbibliotheken, keine Shell und keine Paketmanager. Google bietet vorgefertigte Images:
gcr.io/distroless/static-debian12gcr.io/distroless/python3-debian12gcr.io/distroless/nodejs-debian12
Vorteile:
- Minimaler Angriffsvektor.
- Kleine Images.
- Keine interaktive Shell.
Nachteile:
- Kein
docker execfür Debug. - Dateien können nicht über
RUNinstalliert werden.
Build-Cache nutzen
Reihenfolge beachten, um Caching zu maximieren:
- Abhängigkeitsdateien kopieren.
- Abhängigkeiten installieren.
- Quellcode kopieren.
- Build ausführen.
Mehrere Build-Stages
FROM node:20 AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20 AS build
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
CMD ["node", "dist/main.js"]
Grössenvergleich
| Ansatz | Image-Grösse |
|---|---|
| Einfaches Build-Image | 1 GB+ |
| Multistage mit Slim | 200 MB |
| Multistage mit Distroless | 20 MB |
Sicherheit
- Keine Build-Tools im Runtime-Image.
- Keine Compiler oder Header-Dateien.
- Keine Entwicklungsabhängigkeiten.
- Keine
.git- oder Testverzeichnisse. - Non-root User nutzen.
- Read-only Dateisystem, wo möglich.
Typische Stolpersteine
- Falsche Pfade:
--fromStage-Name falsch geschrieben. - Abhängigkeiten fehlen: Laufzeitbibliothek nicht im Runtime-Image.
- Shell fehlt: Distroless macht
docker execunmöglich. - Dateien vergessen: Nötige Assets nicht kopiert.
- Build-Cache: Zu häufiger Quellcode-Kopieren vor Install-Schritt.
- Multi-Arch: Keine
BUILDPLATFORMAngabe.
Weiterführende Links und Infos
- BotServ.de Docker Image-Optimierung
- BotServ.de Docker Befehle
- BotServ.de Docker Sicherheit
- BotServ.de Docker Rootless
FAQ: Multistage-Builds
Brauche ich Multistage-Builds? Empfehlenswert für kompilierte Sprachen und wenn Image-Grösse oder Sicherheit wichtig sind.
Sind Distroless-Images für Homelabs geeignet? Ja, aber das Debuggen ist schwieriger.
Kann ich mehr als zwei Stages nutzen? Ja, beliebig viele.
Was passiert mit Zwischenstages? Sie sind verfügbar, solange nicht gecleant oder gepruned.
Sind Alpine und Distroless gleich? Nein. Alpine ist klein, aber hat Shell und Paketmanager. Distroless ist noch minimaler.
Quellen und weiterführende Literatur
- Docker Multistage: https://docs.docker.com/build/building/multi-stage/
- Distroless: https://github.com/GoogleContainerTools/distroless
- Alpine: https://hub.docker.com/_/alpine
Zusammenfassung: Docker Multistage-Builds
Multistage-Builds trennen Build- und Laufzeitumgebung und erzeugen kleinere, sicherere Docker-Images. Für kompilierte Sprachen sind sie nahezu unverzichtbar. Durch den Einsatz von Distroless- oder Alpine-Laufzeit-Images kann die Sicherheitsfläche zusätzlich verkleinert werden. Wichtig sind saubere Pfade, vollständige Laufzeitbibliotheken und gelegentliches Prüfen der finalen Image-Grösse. Wer Multistage-Builds einsetzt, verbessert sowohl Build-Zeiten als auch den Betrieb.


