🐳 Docker 🔧 Compose 🔐 Secrets ⚙️ CI/CD DevOps & Container

Docker Multistage Builds mit Claude Code 2026

Docker Multistage Builds für schlanke Production-Images — Claude Code optimiert Node.js und Python Container mit Layer-Caching, Health Checks und Secrets.

📅 6. Mai 2026 ⏱ 11 min Lesezeit 🏷 DevOps & Container

📋 Inhaltsverzeichnis

  1. Multistage Grundlagen
  2. Layer-Caching Optimierung
  3. Node.js Production-Image
  4. Docker Compose
  5. Health Checks und Secrets
  6. CI/CD Integration

🐳 Was du lernst

1. Multistage Grundlagen

Klassische Dockerfiles installieren Build-Tools, Compiler und Dev-Dependencies in dasselbe Image, das später in Production läuft. Das Ergebnis: aufgeblähte Images mit hunderten ungenutzter Pakete und erweiterten Angriffsflächen. Multistage Builds lösen dieses Problem elegant — jede FROM-Anweisung startet eine neue Build-Stage, und du kannst gezielt nur das kopieren, was du wirklich brauchst.

Claude Code versteht Docker-Kontexte auf Anhieb. Du beschreibst deine Stack-Anforderungen und Claude generiert ein optimiertes Multistage-Dockerfile — inklusive korrekter Stage-Namen, COPY --from-Referenzen und passender Base-Images.

Dockerfile — Node.js Multistage (deps → builder → runner) Dockerfile
# ── Stage 1: Abhängigkeiten installieren ──────────────────────
FROM node:20-alpine AS deps

WORKDIR /app

# Nur package-Dateien kopieren → Cache-Layer stabil halten
COPY package.json package-lock.json ./
RUN npm ci --only=production

# ── Stage 2: Build ────────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

# ── Stage 3: Minimales Production-Image ───────────────────────
FROM node:20-alpine AS runner

ENV NODE_ENV=production
WORKDIR /app

# Non-Root User für Sicherheit
RUN addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

# Nur Build-Artefakte + Prod-Deps aus vorherigen Stages
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public

USER nextjs
EXPOSE 3000
CMD ["node", "dist/server.js"]

Das Schlüsselkonzept: COPY --from=builder zieht Dateien aus einer vorherigen Stage in die aktuelle. Build-Tools wie TypeScript-Compiler, Webpack oder Babel landen damit nie im Production-Image. Ein typisches Next.js-Image schrumpft von 1,4 GB auf 180 MB — und ein reines API-Backend kann sogar unter 80 MB landen.

Dockerfile — Python Multistage (builder → production) Dockerfile
# ── Stage 1: Python Build-Umgebung ────────────────────────────
FROM python:3.12-slim AS builder

WORKDIR /app

# Build-Dependencies nur in dieser Stage
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# ── Stage 2: Schlankes Production-Image ───────────────────────
FROM python:3.12-slim AS production

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PATH=/home/appuser/.local/bin:$PATH

WORKDIR /app

RUN adduser --disabled-password --gecos '' appuser

# Nur installierte Packages aus Builder kopieren
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .

USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Claude Code Tipp: Frag Claude einfach: "Erstelle ein Multistage Dockerfile für meine FastAPI-App mit PostgreSQL-Abhängigkeiten, Non-Root User und unter 150 MB." Claude analysiert dein requirements.txt und schlägt optimierte Stage-Grenzen vor.

2. Layer-Caching Optimierung

Docker baut jeden Layer neu, sobald sich ein darüber liegender Layer ändert. Die wichtigste Regel: Was sich selten ändert, kommt zuerst. Package-Dateien ändern sich weit seltener als Quellcode — also immer package.json vor dem eigentlichen Code kopieren.

Layer-Reihenfolge: Falsch vs. Richtig Dockerfile
# ❌ SCHLECHT: Jede Code-Änderung invalidiert npm install
COPY . .
RUN npm ci

# ✅ GUT: package.json-Layer bleibt im Cache solange sich Dependencies nicht ändern
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

Das .dockerignore-File ist ebenso kritisch: Ohne es landet node_modules im Build-Kontext und invalidiert den Cache bei jeder lokalen Installation.

.dockerignore — vollständiges Beispiel text
# Node.js
node_modules
npm-debug.log*
.npm

# Build-Outputs
dist
build
.next
out

# Dev-Tools
.git
.gitignore
.github
*.md
.env*

# Test-Artefakte
coverage
.nyc_output
*.test.ts
__tests__

# IDE
.vscode
.idea
*.log

Für maximale Cache-Ausnutzung aktiviere BuildKit und nutze den --mount=type=cache-Mount direkt im RUN-Befehl. Das npm- oder pip-Cache-Verzeichnis überlebt dann zwischen Builds:

BuildKit Cache-Mounts für npm und pip Dockerfile
# syntax=docker/dockerfile:1.5 muss erste Zeile sein für BuildKit-Features
# syntax=docker/dockerfile:1.5
FROM node:20-alpine AS deps

WORKDIR /app
COPY package.json package-lock.json ./

# npm-Cache bleibt zwischen Builds erhalten — kein Re-Download
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

---
# Python-Variante mit pip-Cache
FROM python:3.12-slim AS builder

COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --user -r requirements.txt
Cache-Busting vermeiden: Kein apt-get update ohne apt-get install in derselben RUN-Anweisung — sonst benutzt Docker einen veralteten apt-Cache. Immer kombinieren: RUN apt-get update && apt-get install -y pkg && rm -rf /var/lib/apt/lists/*

3. Node.js Production-Image

Für Production-Node.js-Images stehen drei Base-Image-Strategien zur Wahl: alpine (klein, musl-libc), slim (Debian-minimal) und distroless (nur Node-Runtime, kein Shell). Claude Code empfiehlt je nach Sicherheitsanforderung:

Produktionsreifes Node.js-Image mit dumb-init und Non-Root User Dockerfile
# syntax=docker/dockerfile:1.5
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production && npm cache clean --force

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

# ── Production Stage ───────────────────────────────────────────
FROM node:20-alpine AS runner

# dumb-init: PID-1-Problem lösen, Signals korrekt weiterleiten
RUN apk add --no-cache dumb-init

ENV NODE_ENV=production \
    PORT=3000

# Non-Root User anlegen
RUN addgroup -g 1001 -S nodejs \
    && adduser -S nextjs -u 1001 -G nodejs

WORKDIR /app

# Artefakte aus vorherigen Stages übernehmen
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs

EXPOSE 3000

# dumb-init als Entrypoint → korrekte Signal-Behandlung (SIGTERM, SIGINT)
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "dist/server.js"]

Warum dumb-init? Node.js als PID 1 im Container ignoriert standardmäßig SIGTERM-Signale. dumb-init fungiert als schlanker Init-Prozess, der Signals korrekt weiterleitet und Zombie-Prozesse aufräumt. Alternativ funktioniert tini (ist in offiziellen Docker-Images vorinstalliert und via --init Flag aktivierbar).

Distroless-Variante für maximale Sicherheit Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production

# Distroless: kein shell, kein apt, minimal CVE-Fläche
FROM gcr.io/distroless/nodejs20-debian12 AS runner

WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist

# Distroless hat built-in non-root user (uid 65532)
USER 65532

EXPOSE 3000
CMD ["dist/server.js"]
Image-Größen-Vergleich (Express.js-API): Standard-Node-Image ~1,1 GB → Alpine-Multistage ~78 MB → Distroless-Multistage ~62 MB. Claude Code kann docker image ls-Output analysieren und konkrete Reduktionsvorschläge machen.

4. Docker Compose

Docker Compose orchestriert mehrere Services, Networks und Volumes in einer einzigen YAML-Datei. Claude Code generiert dabei nicht nur die Grundkonfiguration, sondern schlägt auch depends_on-Conditions mit Health Checks, sinnvolle Network-Segmentierung und Environment-Management mit env_file vor.

docker-compose.yml — vollständiges Produktionsbeispiel YAML
version: '3.9'

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner
      cache_from:
        - ghcr.io/myorg/myapp:cache
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - DATABASE_URL=postgresql://app:${DB_PASSWORD}@postgres:5432/appdb
      - REDIS_URL=redis://redis:6379
    ports:
      - "3000:3000"
    networks:
      - backend
      - frontend
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    profiles: ["app", "full"]

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    profiles: ["db", "full"]

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    profiles: ["cache", "full"]

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    networks:
      - frontend
    depends_on:
      api:
        condition: service_healthy
    profiles: ["proxy", "full"]

volumes:
  pgdata:
  redisdata:

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

Profiles ermöglichen selektives Starten von Service-Gruppen: docker compose --profile db up -d startet nur Postgres, --profile full die komplette Stack. Ideal für Entwicklungsumgebungen wo nicht jeder Service lokal laufen muss.

Claude Code + Compose: Zeig Claude dein bestehendes Compose-File und frag: "Füge Health Checks für alle Services hinzu und konfiguriere depends_on mit service_healthy." Claude erkennt fehlende Health-Check-Patterns und ergänzt sie service-spezifisch.

5. Health Checks und Secrets

Health Checks und sicheres Secret-Management sind zwei der kritischsten Production-Anforderungen, die in Dockerfiles oft vernachlässigt werden.

HEALTHCHECK in Dockerfiles:

HEALTHCHECK — verschiedene Ansätze Dockerfile
# Node.js HTTP-Endpoint prüfen (wget ist in alpine verfügbar)
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

# Alternativ mit curl (wenn installiert)
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

# Python FastAPI
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

# Postgres direkt
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
    CMD pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} || exit 1

Build-Secrets sicher übergeben: Das größte Sicherheitsrisiko bei Docker-Builds ist das Einbacken von Secrets in Image-Layer. Mit BuildKit-Secrets bleibt der Secret-Inhalt komplett außerhalb des Image-Dateisystems:

Build-Secrets mit --mount=type=secret (kein Layer-Leak) Dockerfile
# syntax=docker/dockerfile:1.5
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./

# NPM_TOKEN nur während RUN verfügbar, landet NICHT im Layer
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN} \
    && npm ci \
    && npm config delete //registry.npmjs.org/:_authToken

COPY . .
RUN npm run build

---
# Build-Aufruf mit Secret:
# docker build --secret id=npm_token,src=.npmrc .
# ODER aus Umgebungsvariable:
# echo "$NPM_TOKEN" | docker build --secret id=npm_token,src=- .
Runtime-Secrets via Docker Swarm Secrets oder externe Vault Dockerfile
# Multistage mit Secret-Injection zur Laufzeit (nicht zur Build-Zeit)
FROM node:20-alpine AS runner

ENV NODE_ENV=production

# Secret wird als Datei gemountet (Docker Swarm oder k8s Secret)
# /run/secrets/db_password → process.env wird NICHT verwendet
RUN mkdir -p /run/secrets && chown -R node:node /run/secrets

COPY --from=builder /app/dist ./dist
COPY scripts/read-secrets.js ./scripts/

# Startup-Script liest /run/secrets/* und setzt env vars
USER node
CMD ["node", "scripts/read-secrets.js", "dist/server.js"]
Anti-Pattern vermeiden: Niemals ENV API_KEY=echterkey123 im Dockerfile oder ARG SECRET_TOKEN ohne --mount=type=secret. ARG-Werte sind in docker history sichtbar und bleiben im Image-Layer. Immer --mount=type=secret für Build-Zeit-Secrets verwenden.

6. CI/CD Integration

GitHub Actions mit der offiziellen docker/build-push-action kombiniert Multistage Builds, Layer-Caching via ghcr.io und Multi-Platform-Builds (amd64 + arm64) in einer robusten Pipeline.

.github/workflows/docker-build.yml — vollständige CI/CD-Pipeline YAML
name: Docker Build & Push

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # QEMU für Multi-Platform (arm64 emulation auf amd64 Runner)
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver-opts: image=moby/buildkit:latest

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker Metadata (Tags & Labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          target: runner
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # Layer-Cache aus ghcr.io — kein separater Cache-Service nötig
          cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache
          cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache,mode=max
          secrets: |
            npm_token=${{ secrets.NPM_TOKEN }}

Das cache-from/cache-to-Pattern mit mode=max speichert alle Intermediate-Layer im ghcr.io-Cache — nicht nur das finale Image. Dadurch profitieren auch frühe Stages wie deps vom Cache, selbst wenn sich spätere Stages ändern. In der Praxis reduziert das CI-Build-Zeiten für Node.js-Apps von 8 Minuten auf unter 90 Sekunden.

Erweitert: Security-Scan + Trivy in der CI-Pipeline YAML
# Ergänzung zum Build-Job: Vulnerability-Scan nach dem Build
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: '1'

      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

🚀 Best Practices — Checkliste

  • Multistage: Builder-Stage trennen, nur Artefakte kopieren
  • .dockerignore vollständig: node_modules, .git, .env*, test-Dateien
  • Layer-Reihenfolge: package.json → npm install → Quellcode → Build
  • Non-Root User immer anlegen und aktivieren
  • dumb-init oder tini für korrekte Signal-Behandlung
  • HEALTHCHECK in jedem Production-Dockerfile
  • Secrets ausschließlich via --mount=type=secret (niemals ARG/ENV)
  • CI: ghcr.io-Cache mit mode=max für alle Intermediate-Layer
  • Multi-Platform: linux/amd64,linux/arm64 für Apple Silicon + Cloud
  • Trivy-Scan in CI-Pipeline integrieren

Claude Code für deine Docker-Workflows

Claude Code analysiert deine bestehenden Dockerfiles, identifiziert Optimierungspotenziale und generiert produktionsreife Multistage Builds — direkt in deinem Terminal, ohne Kontextwechsel.

Jetzt kostenlos testen →