📋 Inhaltsverzeichnis
🐳 Was du lernst
- Multistage Dockerfiles für Node.js und Python — von 1 GB auf unter 100 MB
- Layer-Caching richtig nutzen: Build-Zeiten von 4 min auf 45 s reduzieren
- Non-Root User, dumb-init und distroless für sichere Production-Images
- Docker Compose mit Health Checks, Profiles und Secrets
- Build-Secrets sicher übergeben ohne Leak in Image-Layer
- GitHub Actions Multi-Platform-Builds mit ghcr.io-Cache
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.
# ── 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.
# ── 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"]
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.
# ❌ 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.
# 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:
# 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
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:
- alpine — 5 MB Base, ideal für die meisten Apps; native Addons können Probleme machen
- slim — 75 MB, volle glibc-Kompatibilität, besser für native Node.js-Addons
- distroless — kein Shell, minimale CVE-Fläche, perfekt für High-Security-Umgebungen
# 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).
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"]
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.
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.
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:
# 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:
# 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=- .
# 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"]
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.
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.
# 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 →