Zum Inhalt springen

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.

4 Min. Lesezeit
Terminal, das den Start von Docker-Compose-Services mit Logs für jeden Container zeigt

"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.

ymlyaml
# 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 — pgdata hält Daten über Neustarts hinweg persistiert. Ohne es verlierst du deine Datenbank jedes Mal, wenn du docker compose down ausführst.
  • Health Checks für Abhängigkeiten — depends_on mit condition: service_healthy verhindert, 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.

dockerfiledockerfile
# 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"]
dockerfiledockerfile
# ❌ 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.

jsjavascript
// 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):

jsonjson
{
  "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.

ymlyaml
# 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 conflicts

Für produktionsnahe Tests verwende eine explizite Override-Datei:

ymlyaml
# docker-compose.prod.yml
services:
  app:
    build:
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
    volumes: []  # No source mount — use built image
shbash
# Development (default)
docker compose up
 
# Production-like testing
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Datenbankverwaltung

Entwicklungsdatenbanken brauchen Seeding, Migrationen und gelegentliche Resets. Automatisiere das mit Compose-Befehlen.

ymlyaml
# 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
shbash
# 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 seed

Die 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.

ymlyaml
# docker-compose.yml
services:
  app:
    command: node --inspect=0.0.0.0:9229 node_modules/.bin/next dev
    ports:
      - "3000:3000"
      - "9229:9229"
jsonjson
// .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

ymlyaml
# ❌ 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}
shbash
# .env (add to .gitignore)
DB_PASSWORD=local-dev-only-password

Weitere 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:latest heute ist nicht postgres:latest morgen. Versionen pinnen.

Wichtige Erkenntnisse

  1. Produktionsinfrastruktur spiegeln — gleiche Datenbankversion, gleiche Services, gleiche Konfiguration
  2. Abhängigkeitsinstallation vom Quellcode trennen — die Layer-Reihenfolge im Dockerfile hält Neuaufbauten schnell
  3. Quellcode als Volume mounten für Hot Reloading — keine Quelldateien in der Entwicklung per COPY einfügen
  4. Health Checks für Abhängigkeiten verwenden — depends_on ohne Bedingungen wartet nicht auf Readiness
  5. Profiles für Utility-Services nutzen — Migrationen und Seeds sollten nicht automatisch starten
  6. Image-Versionen pinnen — postgres:16-alpine, nicht postgres:latest
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX