Docker Compose für die lokale Entwicklung: Ein vollständiger Leitfaden
Wie du ein produktives Docker-Compose-Setup für die lokale Entwicklung mit Hot Reloading, Datenbank-Persistenz und Parität zur Produktion aufbaust.

"But it works on my machine" war nicht mehr akzeptabel, als Docker das Problem der Umgebungsparität löste. Trotzdem haben viele Teams noch immer eine 15-Schritte-README für das lokale Setup, unterschiedliche Node-Versionen auf verschiedenen Laptops und eine PostgreSQL-Version in der Entwicklung, die nicht zur Produktion passt.
Docker Compose packt deinen gesamten Entwicklungs-Stack — Anwendung, Datenbank, Cache, Queue — in einen einzigen docker compose up-Befehl. Die Herausforderung besteht darin, das ohne Einbußen bei der Developer Experience zu tun. Niemand wird ein Setup nutzen, das zwei Minuten zum Neuaufbau braucht, wenn man eine Zeile Code ändert.
Die Basis-Konfiguration
Beginne mit einer docker-compose.yml, die deine Produktionsinfrastruktur widerspiegelt. Jeder Dienst, von dem deine Anwendung abhängt, erhält einen Container.
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://devuser:devpass@db:5432/appdb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d appdb"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:Wichtige Entscheidungen in dieser Konfiguration:
- Benanntes Volume für PostgreSQL —
pgdatahält Daten über Neustarts hinweg persistiert. Ohne es verlierst du deine Datenbank jedes Mal, wenn dudocker compose downausführst. - Health Checks für Abhängigkeiten —
depends_onmitcondition: service_healthyverhindert, dass die App startet, bevor die Datenbank bereit ist. - Port-Mapping — Exponiere Ports für direkten Datenbankzugriff mit Tools wie pgAdmin oder TablePlus.
Development-Dockerfile
Das Development-Dockerfile unterscheidet sich vom Produktions-Dockerfile. Es priorisiert Neuaufbau-Geschwindigkeit und Hot Reloading gegenüber Image-Größe und Sicherheit.
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
# Install dependencies first — cached unless package.json changes
COPY package.json package-lock.json ./
RUN npm ci
# Don't copy source files — they're mounted as a volume
# This means changes are reflected immediately
EXPOSE 3000
CMD ["npm", "run", "dev"]# ❌ Anti-pattern — COPY everything, then install
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["npm", "run", "dev"]
# Every source file change invalidates the npm ci cache
# ✅ Layer ordering — dependencies cached separately
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Source files come from the volume mount, not COPY
CMD ["npm", "run", "dev"]Das Volume-Mounting (- .:/app) mapped deinen lokalen Quellcode in den Container. Das zweite Volume (- /app/node_modules) verhindert, dass das lokale node_modules das des Containers überschreibt — es könnte plattformspezifische Binärdateien enthalten.
Hot-Reloading-Konfiguration
Hot Reloading in Docker erfordert, dass der File Watcher Änderungen vom Volume-Mount erkennt. Einige Tools brauchen dafür eine explizite Konfiguration.
// next.config.js — Next.js in Docker
module.exports = {
webpack: (config) => {
// Enable polling-based file watching for Docker volumes
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
};
return config;
},
};Für Tools, die chokidar verwenden (Vite, nodemon, webpack-dev-server):
{
"scripts": {
"dev": "CHOKIDAR_USEPOLLING=true next dev"
}
}Polling ist etwas CPU-intensiver als native Dateisystem-Events, aber es ist die einzige zuverlässige Option für Docker-Volume-Mounts unter macOS und Windows.
Umgebungsspezifische Overrides
Verwende docker-compose.override.yml für entwicklerspezifische Einstellungen, die nicht committet werden sollen. Docker Compose merged sie automatisch mit der Basisdatei.
# docker-compose.override.yml (gitignored)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Node.js debugger
db:
ports:
- "5433:5432" # Custom port to avoid conflictsFür produktionsnahe Tests verwende eine explizite Override-Datei:
# docker-compose.prod.yml
services:
app:
build:
dockerfile: Dockerfile
environment:
- NODE_ENV=production
volumes: [] # No source mount — use built image# Development (default)
docker compose up
# Production-like testing
docker compose -f docker-compose.yml -f docker-compose.prod.yml upDatenbankverwaltung
Entwicklungsdatenbanken brauchen Seeding, Migrationen und gelegentliche Resets. Automatisiere das mit Compose-Befehlen.
# docker-compose.yml — add utility services
services:
migrate:
build:
context: .
dockerfile: Dockerfile.dev
command: npx prisma migrate deploy
environment:
- DATABASE_URL=postgresql://devuser:devpass@db:5432/appdb
depends_on:
db:
condition: service_healthy
profiles:
- tools
seed:
build:
context: .
dockerfile: Dockerfile.dev
command: npx prisma db seed
environment:
- DATABASE_URL=postgresql://devuser:devpass@db:5432/appdb
depends_on:
db:
condition: service_healthy
profiles:
- tools# Run migrations
docker compose --profile tools run --rm migrate
# Seed the database
docker compose --profile tools run --rm seed
# Reset everything — database, volumes, containers
docker compose down -v
docker compose up -d
docker compose --profile tools run --rm migrate
docker compose --profile tools run --rm seedDie Einstellung profiles: [tools] bedeutet, dass diese Services nicht mit docker compose up starten. Sie laufen nur bei explizitem Aufruf — der Standardstart bleibt so sauber.
Debugging in Containern
Hänge einen Debugger an den Node.js-Prozess im Docker-Container an, indem du den Debug-Port exponierst und deine IDE konfigurierst.
# docker-compose.yml
services:
app:
command: node --inspect=0.0.0.0:9229 node_modules/.bin/next dev
ports:
- "3000:3000"
- "9229:9229"// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker: Attach",
"type": "node",
"request": "attach",
"port": 9229,
"remoteRoot": "/app",
"localRoot": "${workspaceFolder}",
"restart": true
}
]
}Das Flag --inspect=0.0.0.0:9229 bindet den Debugger an alle Interfaces im Container (nicht nur localhost) und macht ihn damit vom Host aus erreichbar.
Häufige Fallstricke
# ❌ Hardcoded secrets in docker-compose.yml
services:
db:
environment:
POSTGRES_PASSWORD: my-real-password-123
# ✅ Use .env file (gitignored)
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}# .env (add to .gitignore)
DB_PASSWORD=local-dev-only-passwordWeitere Fallstricke, die du vermeiden solltest:
- Fehlendes Volume für node_modules — Host-Module überschreiben Container-Module und verursachen plattformspezifische Fehler
- Keine Health Checks für Datenbanken — die App startet, bevor die Datenbank bereit ist, und scheitert bei der ersten Abfrage
- Verwendung von
latest-Tags —postgres:latestheute ist nichtpostgres:latestmorgen. Versionen pinnen.
Wichtige Erkenntnisse
- Produktionsinfrastruktur spiegeln — gleiche Datenbankversion, gleiche Services, gleiche Konfiguration
- Abhängigkeitsinstallation vom Quellcode trennen — die Layer-Reihenfolge im Dockerfile hält Neuaufbauten schnell
- Quellcode als Volume mounten für Hot Reloading — keine Quelldateien in der Entwicklung per COPY einfügen
- Health Checks für Abhängigkeiten verwenden —
depends_onohne Bedingungen wartet nicht auf Readiness - Profiles für Utility-Services nutzen — Migrationen und Seeds sollten nicht automatisch starten
- Image-Versionen pinnen —
postgres:16-alpine, nichtpostgres:latest


