Docker mit Claude Code: TypeScript-Apps containerisieren 2026

Docker und TypeScript sind ein starkes Duo — aber sauber containerisierte Node.js-Anwendungen zu bauen erfordert Wissen über Multi-Stage Builds, Layer-Caching, Secrets-Management und CI/CD-Pipelines. Claude Code kann dabei als intelligenter Pair-Programmer helfen: von der ersten Dockerfile-Zeile bis zum automatisierten GitHub-Actions-Workflow. In diesem Artikel zeigen wir Best Practices 2026.

BUILD 1. Optimales Dockerfile für Node.js/TypeScript

Das größte Problem bei naiven Node.js-Dockerfiles: der fertige Container enthält TypeScript-Compiler, Dev-Dependencies und Tausende unnötige Dateien. Das Ergebnis sind Images von 1–2 GB, lange Pull-Zeiten und eine größere Angriffsfläche. Der Lösungsansatz: Multi-Stage Builds.

Claude Code kann auf Anfrage vollständige, produktionsreife Dockerfiles generieren. Ein typischer Prompt: "Erstelle ein Multi-Stage Dockerfile für eine TypeScript Express-App mit distroless als Runtime-Image und non-root User."

Zwei-Stage-Ansatz: Builder und Runner

Multi-Stage distroless non-root
# ============================================================ # Stage 1: Builder — kompiliert TypeScript → JavaScript # ============================================================ FROM node:20-alpine AS builder # Arbeitsverzeichnis im Container setzen WORKDIR /app # Nur package-Dateien kopieren (für optimales Layer-Caching) COPY package.json package-lock.json tsconfig.json ./ # Alle Dependencies installieren (inkl. devDependencies für tsc) RUN npm ci --frozen-lockfile # Quellcode kopieren und TypeScript kompilieren COPY src/ ./src/ RUN npm run build # Nur Production-Dependencies für das Runtime-Image installieren RUN npm ci --frozen-lockfile --omit=dev # ============================================================ # Stage 2: Runner — schlankes distroless Production-Image # ============================================================ FROM gcr.io/distroless/nodejs20-debian12 AS runner WORKDIR /app # Non-root User: distroless hat nonroot (UID 65532) eingebaut USER nonroot:nonroot # Nur kompiliertes JS + node_modules aus Builder übernehmen COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules COPY --from=builder --chown=nonroot:nonroot /app/package.json ./ # Port deklarieren (Dokumentation, kein Binding) EXPOSE 3000 # Healthcheck für Container-Orchestration HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD ["/nodejs/bin/node", "-e", "require('http').get('http://localhost:3000/health',(r)=>{process.exit(r.statusCode===200?0:1)})"] # Startbefehl CMD ["dist/index.js"]
Warum distroless? Google distroless-Images enthalten nur die Laufzeit (Node.js) — kein Shell, kein Package-Manager, kein curl. Das reduziert die Angriffsfläche drastisch und schrumpft das Image auf ~120 MB statt ~800 MB bei node:20. Claude Code kann erklären, welches distroless-Image zu welchem Node-Release passt.

tsconfig.json für Docker-kompatible Builds

// tsconfig.json — optimiert für Docker Multi-Stage Build { "compilerOptions": { "target": "ES2022", "module": "CommonJS", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": false, // Im Docker-Build nicht nötig "sourceMap": false, // Production: source maps aus "removeComments": true // Kleinere Output-Dateien }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "tests"] }

Mit Claude Code lässt sich das Dockerfile schrittweise optimieren: Frage beispielsweise nach der besten Base-Image-Version, nach BuildKit-Optimierungen oder nach dem Unterschied zwischen CMD und ENTRYPOINT für TypeScript-Apps.

CACHE 2. .dockerignore & Layer-Caching

Eine schlecht konfigurierte .dockerignore-Datei ist einer der häufigsten Gründe für langsame Docker-Builds. Werden node_modules oder .git in den Build-Kontext kopiert, sendet Docker Hunderte MB an Daten an den Daemon — auch wenn diese Dateien gar nicht gebraucht werden.

Vollständige .dockerignore für TypeScript-Projekte

# .dockerignore — verhindert unnötige Dateien im Build-Kontext # Dependencies — werden im Container neu installiert node_modules npm-debug.log* yarn-debug.log* yarn-error.log* # Build-Output — wird im Builder-Stage neu kompiliert dist build *.tsbuildinfo # Git und VCS .git .gitignore .gitattributes # Entwicklungstools .vscode .idea .editorconfig .eslintcache # Tests und Coverage __tests__ *.test.ts *.spec.ts coverage .nyc_output # Environment-Dateien — NIEMALS im Image! .env .env.* *.local # Docker-Dateien selbst Dockerfile* docker-compose*.yml .dockerignore # Dokumentation README.md docs/ *.md # CI/CD und Scripts .github scripts/ Makefile

Layer-Caching: Die Reihenfolge entscheidet

Docker cached jeden Layer. Ändert sich eine Datei, werden alle folgenden Layers neu gebaut. Das Ziel: selten ändernde Dateien zuerst kopieren, häufig ändernde Dateien zuletzt.

# FALSCH: Quellcode vor package.json kopieren # → jede src-Änderung invalidiert npm install (dauert 2-3 Min) COPY . . # ❌ Alles auf einmal RUN npm ci # RICHTIG: package.json zuerst → npm ci wird gecached COPY package.json package-lock.json ./ # ✅ Selten geändert RUN npm ci --frozen-lockfile # ✅ Nur bei Lock-Änderung neu COPY src/ ./src/ # ✅ Häufig geändert, aber npm ist gecached RUN npm run build # Cache-Buster für externe Ressourcen (z.B. apt-get): ARG CACHE_BUST=1 RUN apt-get update && apt-get install -y curl # Mit CACHE_BUST invalidierbar
BuildKit aktivieren: Setze DOCKER_BUILDKIT=1 als Umgebungsvariable oder nutze docker buildx build. BuildKit parallelisiert unabhängige Stages und bietet besseres Cache-Management. Claude Code nutzt BuildKit automatisch wenn es .devcontainer-Konfigurationen erstellt.
Häufiger Fehler: npm install statt npm ci im Dockerfile verwenden. npm ci ist deterministisch (liest exakt package-lock.json), schneller und schlägt fehl wenn der Lock-File nicht konsistent ist — ideal für Produktions-Builds.

COMPOSE 3. Docker Compose für lokale Entwicklung

In der lokalen Entwicklung brauchen TypeScript-Apps oft eine Datenbank, einen Cache und weitere Services. Docker Compose koordiniert diese Services, definiert Netzwerke und Volumes — alles deklarativ in einer YAML-Datei.

PostgreSQL Redis Hot-Reload
# docker-compose.yml — lokale Entwicklungsumgebung version: "3.9" services: # ---- Hauptanwendung (TypeScript/Node.js) ---- app: build: context: . dockerfile: Dockerfile target: builder # Dev: Builder-Stage mit ts-node-dev args: NODE_ENV: development command: npx ts-node-dev --respawn --transpile-only src/index.ts ports: - "3000:3000" - "9229:9229" # Node.js Debug-Port environment: NODE_ENV: development DATABASE_URL: postgresql://devuser:devpass@postgres:5432/devdb REDIS_URL: redis://redis:6379 LOG_LEVEL: debug env_file: - .env.local # Lokale Overrides (nicht in Git!) volumes: - ./src:/app/src:ro # Hot-Reload: src read-only im Container - ./tsconfig.json:/app/tsconfig.json:ro depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped networks: - app-network # ---- PostgreSQL Datenbank ---- postgres: image: postgres:16-alpine environment: POSTGRES_USER: devuser POSTGRES_PASSWORD: devpass POSTGRES_DB: devdb ports: - "5432:5432" # Nur lokal exponieren volumes: - postgres-data:/var/lib/postgresql/data - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"] interval: 10s timeout: 5s retries: 5 start_period: 10s networks: - app-network # ---- Redis Cache ---- redis: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning ports: - "6379:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 3 networks: - app-network volumes: postgres-data: # Persistente Daten zwischen Neustarts redis-data: networks: app-network: driver: bridge

Produktions-Override mit docker-compose.prod.yml

# docker-compose.prod.yml — überschreibt dev-Einstellungen version: "3.9" services: app: build: target: runner # Production: distroless Runner-Stage command: [] # CMD aus Dockerfile nutzen environment: NODE_ENV: production volumes: [] # Keine Bind-Mounts in Production ports: - "3000:3000" # Kein Debug-Port deploy: replicas: 2 resources: limits: memory: 512m cpus: '0.5' # Starten mit: # docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Claude Code Tipp: Frage Claude Code nach dem Unterschied zwischen docker-compose.yml und docker-compose.override.yml. Override-Dateien werden automatisch zusammengeführt — ideal für developer-spezifische Konfigurationen, die nicht ins Repository gehören.

HEALTH 4. Health Checks & Graceful Shutdown

Kubernetes, Docker Swarm und Load Balancer benötigen zuverlässige Health-Endpoints, um zu entscheiden, ob ein Container Traffic empfangen darf. Gleichzeitig müssen aktive Verbindungen beim Herunterfahren sauber abgeschlossen werden — Graceful Shutdown verhindert abgebrochene Anfragen.

Health-Endpoint in Express/TypeScript

// src/health.ts — Health Check Endpoint import { Router, Request, Response } from 'express'; import { Pool } from 'pg'; import { createClient } from 'redis'; interface HealthStatus { status: 'ok' | 'degraded' | 'down'; version: string; uptime: number; checks: Record<string, boolean>; } export function createHealthRouter(db: Pool, redis: ReturnType<typeof createClient>): Router { const router = Router(); router.get('/health', async (req: Request, res: Response) => { const checks: Record<string, boolean> = {}; // Datenbank-Check try { await db.query('SELECT 1'); checks.database = true; } catch { checks.database = false; } // Redis-Check try { await redis.ping(); checks.redis = true; } catch { checks.redis = false; } const allHealthy = Object.values(checks).every(Boolean); const status: HealthStatus = { status: allHealthy ? 'ok' : 'degraded', version: process.env.npm_package_version ?? 'unknown', uptime: process.uptime(), checks, }; res.status(allHealthy ? 200 : 503).json(status); }); // Liveness-Probe (minimaler Check: Prozess läuft) router.get('/health/live', (_req, res) => res.status(200).json({ status: 'ok' })); // Readiness-Probe (bereit für Traffic?) router.get('/health/ready', async (_req, res) => { try { await db.query('SELECT 1'); res.status(200).json({ status: 'ready' }); } catch { res.status(503).json({ status: 'not ready' }); } }); return router; }

Graceful Shutdown mit SIGTERM

// src/index.ts — Graceful Shutdown Handler import express from 'express'; import { createServer } from 'http'; const app = express(); const server = createServer(app); // Shutdown-Logik — Docker sendet SIGTERM vor SIGKILL async function gracefulShutdown(signal: string): Promise<void> { console.log(`[SHUTDOWN] Signal ${signal} empfangen — starte Graceful Shutdown`); // 1. Neuen Traffic stoppen (Kubernetes: Pod aus Endpoints entfernen) server.close(async (err) => { if (err) console.error('[SHUTDOWN] Server-Fehler:', err); // 2. Datenbankverbindungen schließen try { await db.end(); console.log('[SHUTDOWN] DB-Pool geschlossen'); } catch (e) { console.error('[SHUTDOWN] DB-Fehler:', e); } // 3. Redis-Verbindung schließen try { await redis.quit(); console.log('[SHUTDOWN] Redis geschlossen'); } catch (e) { console.error('[SHUTDOWN] Redis-Fehler:', e); } console.log('[SHUTDOWN] Graceful Shutdown abgeschlossen'); process.exit(0); }); // Timeout: Falls Shutdown zu lange dauert → force exit setTimeout(() => { console.error('[SHUTDOWN] Timeout — Force Exit'); process.exit(1); }, 30_000).unref(); } // SIGTERM: Docker stop / Kubernetes Pod-Deletion process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // SIGINT: Ctrl+C in Entwicklung process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Unbehandelte Fehler abfangen process.on('uncaughtException', (err) => { console.error('[FATAL] Uncaught Exception:', err); process.exit(1); }); server.listen(3000, () => console.log('[APP] Server läuft auf Port 3000'));
HEALTHCHECK im Dockerfile: Docker markiert einen Container als unhealthy nach retries fehlgeschlagenen Checks. Kubernetes hingegen nutzt eigene Probe-Konfigurationen in der Pod-Definition (livenessProbe, readinessProbe) — beide Mechanismen ergänzen sich.

SECRETS 5. Secrets & Environment Management

Secrets — API-Keys, Datenbankpasswörter, JWT-Secrets — gehören niemals in Docker-Images. Weder als ENV-Anweisung, noch als Build-Argument das in den Layers verbleibt. Claude Code warnt automatisch, wenn Secrets unsicher behandelt werden.

Häufiger Fehler — Secrets in Layern: Selbst wenn ein RUN rm -f /tmp/secret.key nach dem Kopieren folgt, bleibt der Secret in einem vorangegangenen Layer erhalten und ist mit docker history auslesbar.

ARG vs ENV — Der kritische Unterschied

# ❌ FALSCH: ENV ist im fertigen Image sichtbar ENV API_KEY=super-secret-key-123 # Im Image permanent gespeichert! # ❌ FALSCH: ARG landet in docker history ARG API_KEY RUN npm install --token=${API_KEY} # In Layer-Metadaten sichtbar! # ✅ RICHTIG: BuildKit --secret mount (nie im Layer gespeichert) # Dockerfile: RUN --mount=type=secret,id=npm_token \ NPM_TOKEN=$(cat /run/secrets/npm_token) \ npm install # Build-Befehl: # DOCKER_BUILDKIT=1 docker build \ # --secret id=npm_token,src=.npm_token \ # -t my-app . # ✅ RICHTIG: Runtime-Secrets via env_file (nie ins Image) ENV DATABASE_URL="" # Placeholder — Wert kommt zur Laufzeit

Docker Secrets für Swarm/Kubernetes

# docker-compose.yml mit Docker Secrets (Swarm-Modus) version: "3.9" services: app: image: my-app:latest secrets: - db_password - jwt_secret environment: # App liest Secret aus Datei statt Env-Variable DB_PASSWORD_FILE: /run/secrets/db_password JWT_SECRET_FILE: /run/secrets/jwt_secret secrets: db_password: external: true # Erstellt mit: docker secret create db_password ./db_pass.txt jwt_secret: external: true

Secret-Datei in TypeScript lesen

// src/config.ts — Secrets aus Dateien oder Env-Variablen lesen import { readFileSync } from 'fs'; function readSecret(envVar: string): string { // Docker Secrets werden als Dateien in /run/secrets/ gemountet const fileEnvVar = process.env[`${envVar}_FILE`]; if (fileEnvVar) { try { return readFileSync(fileEnvVar, 'utf-8').trim(); } catch (err) { throw new Error(`Secret-Datei nicht lesbar: ${fileEnvVar}`); } } // Fallback auf direkte Env-Variable (Entwicklung) const value = process.env[envVar]; if (!value) throw new Error(`${envVar} nicht gesetzt`); return value; } export const config = { dbPassword: readSecret('DB_PASSWORD'), jwtSecret: readSecret('JWT_SECRET'), port: parseInt(process.env.PORT ?? '3000', 10), nodeEnv: process.env.NODE_ENV ?? 'development', } as const;
Zod für Konfigurationsvalidierung: Claude Code empfiehlt oft zod zur Validierung von Environment-Variablen beim App-Start. So schlägt die Anwendung sofort fehl, wenn kritische Konfiguration fehlt — statt zur Laufzeit mit kryptischen Fehlern.

CI/CD 6. GitHub Actions CI/CD

Eine vollständige CI/CD-Pipeline für TypeScript-Apps baut das Docker-Image, führt Tests aus, pusht zu einer Registry und deployt — alles automatisch bei jedem Merge in den Hauptbranch.

Vollständiger Workflow: Build, Test, Push, Deploy

GHCR Multi-Platform Layer-Cache
# .github/workflows/docker-build-push.yml name: Build, Test & Deploy on: push: branches: [main, develop] pull_request: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: # ---- Job 1: Tests und Linting ---- test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Node.js Setup uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Dependencies installieren run: npm ci - name: TypeScript kompilieren run: npm run build - name: Linting run: npm run lint - name: Unit-Tests run: npm test -- --coverage - name: Coverage hochladen uses: codecov/codecov-action@v4 if: github.event_name == 'push' # ---- Job 2: Docker Build & Push ---- docker: needs: test runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Docker Buildx einrichten uses: docker/setup-buildx-action@v3 - name: Bei GHCR anmelden uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Image-Metadaten extrahieren id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha,prefix=sha- type=ref,event=branch type=semver,pattern={{version}} type=raw,value=latest,enable={{is_default_branch}} - name: Build & Push (Multi-Platform) uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name == 'push' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | BUILD_DATE=${{ github.event.head_commit.timestamp }} GIT_SHA=${{ github.sha }} # ---- Job 3: Deploy (nur main-Branch) ---- deploy: needs: docker runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' && github.event_name == 'push' environment: production steps: - name: Deploy via SSH uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} script: | docker pull ghcr.io/${{ github.repository }}:latest docker compose -f /opt/app/docker-compose.prod.yml up -d --no-deps app docker image prune -f

Wichtige Best Practices für die CI/CD-Pipeline

  • cache-from/cache-to: GitHub Actions Cache reduziert Build-Zeit um 60–80% bei unveränderter package.json
  • Multi-Platform builds: linux/amd64,linux/arm64 für Apple Silicon Macs und AWS Graviton
  • GITHUB_TOKEN: Automatisch verfügbar, kein manuelles Secret für GHCR nötig
  • Conditional push: Bei Pull Requests wird nur gebaut, nicht gepusht
  • Environment-Protection: GitHub Environments erzwingen manuelle Freigabe vor Production-Deploy
  • Image-Pruning: Alte Images nach Deploy entfernen, sonst füllt sich die Festplatte
Claude Code im CI/CD-Kontext: Claude Code kann direkt .github/workflows/*.yml-Dateien erstellen und erklären. Frage beispielsweise: "Erstelle einen GitHub Actions Workflow der auf Push zu main ein Docker-Image baut, zu GHCR pusht und dann per SSH auf meinen Server deployt." Claude Code generiert den vollständigen Workflow inklusive Secret-Referenzen.

Fazit: Docker + TypeScript + Claude Code

Moderne TypeScript-Apps verdienen moderne Container-Setups. Multi-Stage Builds, sauberes Layer-Caching, robuste Health Checks und sichere Secret-Verwaltung sind kein Luxus — sie sind die Grundlage für zuverlässige Production-Deployments.

Claude Code beschleunigt diesen Prozess erheblich: als intelligenter Pair-Programmer erklärt es nicht nur was zu konfigurieren ist, sondern auch warum. Von der ersten Dockerfile-Zeile über Docker-Compose-Setups bis zur vollständigen GitHub-Actions-Pipeline — Claude Code versteht den Kontext und generiert produktionsreife Konfigurationen.

Zusammenfassung der Best Practices 2026
  • Multi-Stage Builds mit distroless Runner-Image (~120 MB statt ~800 MB)
  • Non-root User (nonroot:nonroot in distroless) für Security
  • npm ci --frozen-lockfile statt npm install für reproduzierbare Builds
  • Package-Dateien VOR Quellcode kopieren (Layer-Caching)
  • .dockerignore mit node_modules, .git, .env, dist
  • Health Checks: /health, /health/live, /health/ready
  • SIGTERM-Handler für Graceful Shutdown mit 30s Timeout
  • BuildKit --secret mount für Build-Zeit-Secrets
  • Docker Secrets für Runtime-Secrets (nie als ENV im Image)
  • GitHub Actions mit cache-from/cache-to für schnelle CI-Builds
  • Multi-Platform builds für amd64 + arm64

DevOps-Modul im Kurs

Im Claude Code Mastery Kurs: vollständiges Docker-Modul mit Multi-Stage Builds, Docker Compose, Secrets-Management und GitHub Actions CI/CD für TypeScript-Anwendungen.

14 Tage kostenlos testen →