Skip to content
BotServBotServ
DockerMultistageDockerfileImage-Optimierung

Docker Multistage-Builds

Docker-Images kleiner und sicherer bauen mit Multistage-Builds. Build-, Runtime- und Distroless-Images.

S

schutzgeist

3 min read
Docker Multistage-Builds

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-debian12
  • gcr.io/distroless/python3-debian12
  • gcr.io/distroless/nodejs-debian12

Vorteile:

  • Minimaler Angriffsvektor.
  • Kleine Images.
  • Keine interaktive Shell.

Nachteile:

  • Kein docker exec für Debug.
  • Dateien können nicht über RUN installiert werden.

Build-Cache nutzen

Reihenfolge beachten, um Caching zu maximieren:

  1. Abhängigkeitsdateien kopieren.
  2. Abhängigkeiten installieren.
  3. Quellcode kopieren.
  4. 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

AnsatzImage-Grösse
Einfaches Build-Image1 GB+
Multistage mit Slim200 MB
Multistage mit Distroless20 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: --from Stage-Name falsch geschrieben.
  • Abhängigkeiten fehlen: Laufzeitbibliothek nicht im Runtime-Image.
  • Shell fehlt: Distroless macht docker exec unmöglich.
  • Dateien vergessen: Nötige Assets nicht kopiert.
  • Build-Cache: Zu häufiger Quellcode-Kopieren vor Install-Schritt.
  • Multi-Arch: Keine BUILDPLATFORM Angabe.

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

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.

Zurück zum KI Blog
Share:

Ähnliche Beiträge