Zum Inhalt springen
Docker Fortgeschritten 25 min

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.

Aktualisiert:
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_modules mit 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:

  1. build - hat alle Dev-Tools, baut das Projekt
  2. 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:

  1. deps - nur Dependencies installieren
  2. build - Next.js bauen
  3. 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:

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

  1. FROM (fast nie aendern)
  2. WORKDIR
  3. COPY package*.json (selten)
  4. RUN npm ci (selten)
  5. COPY . . (oft)
  6. 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
  • --chown beim Copy - korrekte Rechte
  • npm prune --production entfernt Dev-Deps nach Build

Zusammenfassung

  • Multi-Stage Builds trennen Build-Tools und Runtime
  • AS name benennt Stages, COPY --from=name kopiert zwischen
  • scratch und distroless fuer minimale Images
  • --target baut 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.

Zurรผck zum Docker Kurs