Multi-Stage Builds
Mit Multi-Stage Builds Docker-Images drastisch verkleinern: Build-Tools im Build-Stage, nur das Endprodukt im finalen Image. Ein Must-have fuer Produktion.
Inhaltsverzeichnis
Multi-Stage Builds
Ein typisches Problem: Um deine App zu bauen, brauchst du Compiler, Node-dev-Dependencies, TypeScript-Compiler etc. Aber in Produktion willst du nur das fertige Ergebnis. Mit Multi-Stage Builds loest du das elegant.
Das Problem
Ein simples TypeScript-Projekt ohne Multi-Stage:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Das Image enthaelt:
node_modulesmit dev dependencies (TypeScript, Build-Tools)- Die Source-Dateien (
src/) - Das fertige Build (
dist/)
Ergebnis: unnoetig gross und mit Artefakten, die nichts mit Produktion zu tun haben.
Multi-Stage zur Rettung
# =========================
# Stage 1: Build
# =========================
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# =========================
# Stage 2: Production
# =========================
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
Zwei Stages:
- build - hat alle Dev-Tools, baut das Projekt
- Final - hat nur das Build-Ergebnis + Prod-Dependencies
Das finale Image enthaelt kein TypeScript, keinen Source-Code, nur Prod-node_modules + dist.
Die Syntax
Stage benennen
FROM node:22-alpine AS build
AS name gibt der Stage einen Namen, auf den du spaeter mit --from=name zugreifst.
Zwischen Stages kopieren
COPY --from=build /app/dist ./dist
Holt Dateien aus der build-Stage in die aktuelle.
Du kannst auch aus externen Images kopieren:
COPY --from=golang:1.23 /usr/local/go /usr/local/go
Ein Go-Beispiel
Go ist der Extremfall - statisches Binary, kein Runtime noetig:
# Build
FROM golang:1.23 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .
# Runtime - minimal!
FROM scratch
COPY --from=build /out/app /app
CMD ["/app"]
Scratch ist das leere Image - wirklich nichts drin. Das finale Image ist oft unter 10 MB.
Distroless fuer mehr Sicherheit
Statt scratch kannst du distroless nehmen - minimales Image mit nur dem Noetigsten (CA-Certs, minimale libc):
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
CMD ["/app"]
Distroless hat keine Shell, keinen Paket-Manager - angenehm fuer Security.
Ein Python-Beispiel
FROM python:3.12 AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=build /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Der Clou: pip install --user installiert in /root/.local. Den Ordner kopierst du in die schlanke Stage.
Next.js-Beispiel
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/public ./public
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
Hier gibt es sogar drei Stages:
- deps - nur Dependencies installieren
- build - Next.js bauen
- Final - nur das fertige Build + static Files
Next.js 15+ hat den Standalone Output - das Final-Image kommt ohne node_modules aus, weil alles ins standalone gepackt ist.
Stage โbuildenโ per Flag
Du kannst gezielt bis zu einer Stage bauen (z.B. zum Debuggen):
docker build --target build -t meine-app:build .
Praktisch zum Testen von Zwischenergebnissen.
Kombinierte Nutzung fuer Dev & Prod
Du kannst eine Dev-Stage und eine Prod-Stage im gleichen Dockerfile haben:
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS dev
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
FROM base AS prod
RUN npm ci --production
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
Dann:
docker build --target dev -t meine-app:dev .
docker build --target prod -t meine-app:prod .
Dev hat alle Tools, Prod ist schlank.
Vergleich: Groessen-Ersparnis
Beispiel TypeScript-Server:
| Variante | Groesse |
|---|---|
| Ohne Multi-Stage | ~1.1 GB |
| Mit Multi-Stage | ~180 MB |
| Mit slim + Multi-Stage | ~95 MB |
| Next.js Standalone | ~55 MB |
Das spart nicht nur Disk-Platz, sondern macht Deployments schneller, Image-Pulls schneller, und reduziert die Angriffsflaeche.
Build-Caching optimieren
Wichtige Regel: Aenderungen, die selten passieren, oben im Dockerfile. Aenderungen, die oft passieren, unten.
Bei Node.js typisch:
FROM(fast nie aendern)WORKDIRCOPY package*.json(selten)RUN npm ci(selten)COPY . .(oft)RUN npm run build(oft)
So bleiben die teuren Dependency-Installs im Cache.
BuildKit - der moderne Builder
Moderne Docker-Versionen nutzen BuildKit standardmaessig. Vorteile:
- Paralleler Build von unabhaengigen Stages
- Besseres Caching
- Secrets handling:
RUN --mount=type=secret,id=npm .npmrc ...
Build Secrets
docker build --secret id=npm,src=$HOME/.npmrc -t meine-app .
Im Dockerfile:
RUN --mount=type=secret,id=npm,target=/root/.npmrc npm ci
Das Secret ist waehrend RUN verfuegbar, aber nicht im Image.
Ein Production-Ready Beispiel
Komplettes Node-Dockerfile mit allen Best Practices:
FROM node:22-alpine AS base
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build && npm prune --production
FROM base AS prod
ENV NODE_ENV=production
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
COPY --from=build --chown=app:app /app/package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]
Features:
- 4 Stages (base, deps, build, prod)
- Non-Root User in prod
--chownbeim Copy - korrekte Rechtenpm prune --productionentfernt Dev-Deps nach Build
Zusammenfassung
- Multi-Stage Builds trennen Build-Tools und Runtime
AS namebenennt Stages,COPY --from=namekopiert zwischenscratchunddistrolessfuer minimale Images--targetbaut gezielt bis zu einer Stage- Cache-freundlich: seltene Aenderungen oben, haeufige unten
- BuildKit-Secrets verhindern, dass Credentials ins Image wandern
Im naechsten Kapitel: docker-compose fuer Multi-Service-Setups mit Datenbank und Cache.