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 →