Docker Compose for Development: Local Infrastructure

Docker Compose for local development: PostgreSQL, Redis, Authentik, networking, health checks, volume management, and environment-specific configurations for TypeScript monorepos.

E
ECOSIRE Research and Development Team
|19. März 20269 Min. Lesezeit1.9k Wörter|

Docker Compose für Entwicklung: Lokale Infrastruktur

Der Unterschied zwischen einer angenehmen Onboarding-Erfahrung („klonen und ausführen pnpm dev:infra“) und einer schmerzhaften („zuerst PostgreSQL einrichten, dann Redis konfigurieren, dann ...“) hängt davon ab, wie gut Ihr Docker Compose-Setup Ihre Infrastrukturanforderungen erfasst. Mit einem gut gestalteten docker-compose.dev.yml kann jeder Entwickler auf jedem Computer innerhalb von Minuten genau dieselbe Infrastruktur ausführen.

In diesem Leitfaden werden die Muster für einen lokalen Entwicklungsstack in Produktionsqualität behandelt: Dienstkonfiguration, Zustandsprüfungen, Netzwerk, Volume-Management und die Integration in die Startsequenz Ihrer Anwendung.

Wichtige Erkenntnisse

– Verwenden Sie lokal einen nicht standardmäßigen Port für PostgreSQL (5433), um Konflikte mit Systeminstallationen zu vermeiden

  • Integritätsprüfungen für Dienstabhängigkeiten verhindern Startfehler „Verbindung abgelehnt“. – Benannte Volumes behalten Datenbankdaten zwischen Container-Neustarts bei – Bind-Mounts funktionieren unter Windows nicht zuverlässig – Verwenden Sie env_file, um Umgebungsvariablen aus Ihrer .env.local-Datei in Container zu laden
  • Trennen Sie docker-compose.dev.yml von docker-compose.prod.yml – sie dienen unterschiedlichen Zwecken – Das depends_on.condition: service_healthy-Muster wartet auf die tatsächliche Bereitschaft, nicht nur auf den Start des Containers
  • Verwenden Sie profiles, um optionale Dienste (E-Mail, Überwachung) zu aktivieren
  • Führen Sie docker compose (v2) und nicht docker-compose (v1) aus – die Plugin-Syntax ist aktuell

Der komplette Entwicklungs-Stack

# infrastructure/docker-compose.dev.yml
name: ecosire-dev

services:
  # ─── PostgreSQL ─────────────────────────────────────────────────
  postgres:
    image: postgres:17-alpine
    container_name: ecosire-postgres
    environment:
      POSTGRES_DB: ecosire_dev
      POSTGRES_USER: ecosire
      POSTGRES_PASSWORD: dev_password_change_in_prod
    ports:
      - "5433:5432"  # 5433 externally — avoids conflicts with system PostgreSQL
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d  # Run SQL on first start
    command: >
      postgres
        -c shared_buffers=256MB
        -c effective_cache_size=1GB
        -c work_mem=16MB
        -c maintenance_work_mem=128MB
        -c checkpoint_completion_target=0.9
        -c wal_buffers=16MB
        -c max_connections=100
        -c log_min_duration_statement=100
        -c log_statement=ddl
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ecosire -d ecosire_dev"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  # ─── Redis ──────────────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    container_name: ecosire-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: >
      redis-server
        --maxmemory 512mb
        --maxmemory-policy allkeys-lru
        --appendonly yes
        --appendfsync everysec
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # ─── Authentik (Identity Provider) ──────────────────────────────
  authentik-server:
    image: ghcr.io/goauthentik/server:2024.12
    container_name: ecosire-authentik
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgres
      AUTHENTIK_POSTGRESQL__USER: ecosire
      AUTHENTIK_POSTGRESQL__PASSWORD: dev_password_change_in_prod
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_SECRET_KEY: dev-secret-key-change-in-production-32chars
      AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
      AUTHENTIK_DISABLE_STARTUP_ANALYTICS: "true"
    volumes:
      - authentik_media:/media
      - authentik_certs:/certs
    ports:
      - "9000:9000"  # HTTP
      - "9443:9443"  # HTTPS
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "ak healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s  # Authentik takes time to initialize
    restart: unless-stopped

  authentik-worker:
    image: ghcr.io/goauthentik/server:2024.12
    container_name: ecosire-authentik-worker
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgres
      AUTHENTIK_POSTGRESQL__USER: ecosire
      AUTHENTIK_POSTGRESQL__PASSWORD: dev_password_change_in_prod
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_SECRET_KEY: dev-secret-key-change-in-production-32chars
    volumes:
      - authentik_media:/media
      - authentik_certs:/certs
      - /var/run/docker.sock:/var/run/docker.sock  # For Authentik's proxy
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  # ─── Mailpit (Email Testing) ─────────────────────────────────────
  mailpit:
    image: axllent/mailpit:latest
    container_name: ecosire-mailpit
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI
    environment:
      MP_MAX_MESSAGES: 200
      MP_SMTP_AUTH_ACCEPT_ANY: true
      MP_SMTP_AUTH_ALLOW_INSECURE: true
    restart: unless-stopped
    profiles:
      - email  # Optional — use `docker compose --profile email up`

  # ─── pgAdmin (Database GUI) ─────────────────────────────────────
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: ecosire-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: admin
      PGADMIN_CONFIG_SERVER_MODE: "False"
      PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
    ports:
      - "5050:80"
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    profiles:
      - tools  # Optional

networks:
  default:
    name: ecosire-dev-network

volumes:
  postgres_data:
    name: ecosire-postgres-data
  redis_data:
    name: ecosire-redis-data
  authentik_media:
    name: ecosire-authentik-media
  authentik_certs:
    name: ecosire-authentik-certs
  pgadmin_data:
    name: ecosire-pgadmin-data

Package.json-Skripte

Verknüpfen Sie die Docker Compose-Befehle mit Ihren Monorepo-Skripten:

{
  "scripts": {
    "dev:infra": "docker compose -f infrastructure/docker-compose.dev.yml up -d",
    "dev:infra:down": "docker compose -f infrastructure/docker-compose.dev.yml down",
    "dev:infra:logs": "docker compose -f infrastructure/docker-compose.dev.yml logs -f",
    "dev:infra:reset": "docker compose -f infrastructure/docker-compose.dev.yml down -v && pnpm dev:infra",
    "dev:infra:email": "docker compose -f infrastructure/docker-compose.dev.yml --profile email up -d",
    "dev:infra:tools": "docker compose -f infrastructure/docker-compose.dev.yml --profile tools up -d"
  }
}

Das Flag --profile lässt optionale Dienste (E-Mail-Testen mit Mailpit, Datenbank-GUI mit pgAdmin) inaktiv bleiben, bis sie explizit angefordert werden.


Datenbankinitialisierungsskripte

Platzieren Sie SQL-Dateien in infrastructure/init-scripts/ – sie werden beim ersten Containerstart ausgeführt:

-- infrastructure/init-scripts/01-create-databases.sql
-- Create all databases Authentik needs separately from the app DB
CREATE DATABASE authentik;
GRANT ALL PRIVILEGES ON DATABASE authentik TO ecosire;

-- Create test database for CI
CREATE DATABASE ecosire_test;
GRANT ALL PRIVILEGES ON DATABASE ecosire_test TO ecosire;
-- infrastructure/init-scripts/02-extensions.sql
-- Enable PostgreSQL extensions
\c ecosire_dev;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";  -- Trigram search
CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- Composite GIN indexes

\c ecosire_test;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

Initialisierungsskripte werden in alphabetischer Reihenfolge ausgeführt. Der psql-Metabefehl \c database_name wechselt die aktive Datenbank.


Integration von Umgebungsvariablen

Ihre Anwendung liest von .env.local im Monorepo-Stammverzeichnis. Die Docker-Dienste müssen wissen, wie sie mithilfe von Dienstnamen (nicht localhost) eine Verbindung zueinander herstellen können:

# .env.local (monorepo root)

# PostgreSQL — use 5433 externally (host) or 5432 internally (container network)
DATABASE_URL=postgresql://ecosire:dev_password_change_in_prod@localhost:5433/ecosire_dev

# Redis
REDIS_URL=redis://localhost:6379

# Authentik — use 9000 for external calls from your dev machine
AUTHENTIK_URL=http://localhost:9000
# Use service name for server-to-server calls within Docker network
AUTHENTIK_INTERNAL_URL=http://authentik-server:9000

# Email (Mailpit SMTP)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=false

# Application
NODE_ENV=development

Für Anwendungen, die in Docker ausgeführt werden und mit anderen Diensten kommunizieren müssen, verwenden Sie Dienstnamen. Verwenden Sie für Anwendungen, die auf Ihrem Hostcomputer ausgeführt werden (NestJS, Next.js im Entwicklungsmodus), localhost mit den vom Host zugeordneten Ports.


Health Checks Deep Dive

Gesundheitsprüfungen verhindern kaskadierende Startfehler. Der depends_on.condition: service_healthy wartet auf die tatsächliche Bereitschaft, nicht nur auf den Start des Containers:

# Without health checks — can fail because PostgreSQL isn't ready
depends_on:
  - postgres

# With health checks — waits for PostgreSQL to accept connections
depends_on:
  postgres:
    condition: service_healthy

Benutzerdefinierte Gesundheitschecks für Ihre eigenen Dienste:

// apps/api/src/health/health.controller.ts
@Get()
@Public()
@HealthCheck()
async check() {
  return this.health.check([
    () => this.db.isHealthy('database'),
    () => this.redis.isHealthy('redis'),
  ]);
}
# If your API is also dockerized
api:
  image: ecosire-api:latest
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"]
    interval: 30s
    timeout: 10s
    retries: 3
    start_period: 30s

Volumenverwaltung

Benannte Volumes speichern Daten zwischen Neustarts. Verstehen Sie, wann die einzelnen Volume-Typen verwendet werden sollten:

Geben Sieein BeharrlichkeitLeistungVerwenden Sie für
Benannter DatenträgerJaAusgezeichnetDatenbankdaten
BindungshalterungJaGut (Linux), Schlecht (macOS)Quellcode Hot-Reload
tmpfsNeinAusgezeichnetTemporäre Dateien, Geheimnisse
# Use bind mounts for source code (enables hot reload)
volumes:
  - ./apps/api/src:/app/src  # Code changes reflected immediately

# Use named volumes for data
volumes:
  - postgres_data:/var/lib/postgresql/data

# Use tmpfs for ephemeral data
volumes:
  - type: tmpfs
    target: /tmp

Unter macOS mit Docker Desktop verwenden Bind-Mounts gRPC FUSE, was deutlich langsamer ist als unter Linux. Führen Sie die NestJS- und Next.js-Entwicklungsserver direkt auf Ihrem Hostcomputer (nicht in Docker) aus, um die Leistung des nativen Dateisystems zu erhalten.


Produktion Docker Compose

Die Produktionskompositionsdatei ist strukturell anders – keine lokalen Ports, Neustartrichtlinien, Produktionsressourcenbeschränkungen:

# infrastructure/docker-compose.prod.yml
name: ecosire-prod

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    # No port mapping — only accessible within Docker network
    restart: always
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 512M

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 1gb
    volumes:
      - redis_data:/data
    restart: always
    # No port mapping — internal only

volumes:
  postgres_data:
  redis_data:

Die Produktion stellt keine Ports extern zur Verfügung – Anwendungen stellen eine Verbindung über das interne Docker-Netzwerk her. Nginx verarbeitet externen Datenverkehr.


Häufige Fallstricke und Lösungen

Falle 1: Portkonflikte mit Systemdiensten

PostgreSQL, Redis und andere Dienste werden häufig als Systemdienste ausgeführt. Ordnen Sie in der Entwicklung immer Nicht-Standard-Ports zu:

  • PostgreSQL: 5433:5432 (nicht 5432:5432)
  • Redis: 6379:6379 behalten (selten Konflikte)
  • Run lsof -i :5432 to check what's using the default port

Falle 2: Probleme mit der Volume-Berechtigung unter Linux

Docker-Volumes unter Linux verwenden standardmäßig Root-Besitz. Wenn Ihr Containerbenutzer kein Root-Benutzer ist, legen Sie den richtigen Besitz fest:

postgres:
  image: postgres:17-alpine
  user: "999:999"  # postgres user's UID:GID
  # Or use init container to fix permissions

Falle 3: Die Authentik-Initialisierung dauert mehr als 60 Sekunden

Authentik führt Datenbankmigrationen beim ersten Start durch. Der start_period: 60s im Gesundheitscheck gibt ihm Zeit. Wenn abhängige Dienste gestartet werden, bevor Authentik bereit ist, schlagen sie fehl. Verwenden Sie die Bedingung service_healthy und geben Sie ihr genügend start_period.

Falle 4: Docker Desktop-Ressourcenbeschränkungen auf dem Mac

Standardmäßig weist Docker Desktop 2 CPUs und 2 GB RAM zu – nicht genug für die gleichzeitige Ausführung von PostgreSQL + Redis + Authentik. Erhöhen Sie die Docker-Desktop-Einstellungen > Ressourcen auf mindestens 4 CPUs und 6 GB RAM.

Falle 5: docker-compose vs. docker compose

Der alte docker-compose (v1, geschrieben in Python) ist veraltet. Verwenden Sie docker compose (v2, das Plugin). Überprüfen Sie Ihre Version: docker compose version. Wenn Sie Docker Compose version v2.x.x sehen, verwenden Sie v2.


Häufig gestellte Fragen

Soll ich meine Anwendungsdienste (NestJS, Next.js) während der Entwicklung in Docker ausführen?

Im Allgemeinen nein – führen Sie bei aktiver Entwicklung Ihre Anwendungsdienste auf Ihrem Host-Computer aus, um das Hot-Reload zu beschleunigen und das Debuggen zu vereinfachen. Verwenden Sie Docker nur für Infrastrukturdienste (Datenbanken, Caches, Identitätsanbieter), die stabil sind und nicht häufig neu gestartet werden müssen. Die Ausnahme besteht, wenn Ihre Anwendung native Abhängigkeiten aufweist, die sich zwischen Ihrem Entwicklungsbetriebssystem und der Produktionsumgebung unterscheiden.

Wie gehe ich mit Datenbankmigrationen im Docker Compose-Workflow um?

Führen Sie Migrationen von Ihrem Hostcomputer aus, nachdem Sie die Infrastruktur gestartet haben: pnpm dev:infra && pnpm db:migrate. Führen Sie während der Entwicklung keine Migrationen innerhalb eines Docker-Containers durch – Sie verlieren die Typprüfung und die IDE-Integration, die Drizzle-Migrationen sicher machen. Verwenden Sie für die erste Datenbankerstellung die initdb.d-Skripte von Docker.

Wie kann ich meine lokalen Docker-Volumes sichern und wiederherstellen?

Verwenden Sie docker run --rm -v postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz /data zum Sichern. Stellen Sie mit dem gleichen Ansatz mit tar xzf wieder her. Für die Entwicklung können Sie auch einen Dump mit pg_dump durchführen und mit psql wiederherstellen, da Sie den Port offengelegt haben.

Wie teile ich den Docker Compose-Status mit anderen Teammitgliedern?

Die Docker Compose-Datei wird über Git geteilt, die Daten in Volumes sind jedoch lokal. Jeder Entwickler beginnt mit einer leeren Datenbank und führt Migrationen/Seeds aus, um diese zu füllen. Verwenden Sie Seed-Skripte (im Repo festgeschrieben), um konsistente Testdaten zu erstellen. Der gemeinsame docker-compose.dev.yml stellt sicher, dass jeder die gleichen Dienstversionen und Konfigurationen verwendet.

Warum Mailpit anstelle von echter E-Mail in der Entwicklung verwenden?

Mailpit ist ein lokaler SMTP-Server, der alle ausgehenden E-Mails erfasst und eine Web-Benutzeroberfläche zur Anzeige dieser E-Mails bereitstellt. Es verhindert, dass während der Entwicklung versehentlich echte E-Mails an echte Benutzer gesendet werden, erfordert keine SMTP-Anmeldeinformationen und ermöglicht die Überprüfung von E-Mail-Vorlagen, ohne Ihren Posteingang zu überprüfen. Konfigurieren Sie Ihre App für die Verwendung von SMTP_HOST=localhost SMTP_PORT=1025 und besuchen Sie http://localhost:8025, um erfasste E-Mails anzuzeigen.


Nächste Schritte

Ein gut ausgearbeitetes Docker Compose-Setup für die lokale Entwicklung ist eine Investition, die sich jedes Mal auszahlt, wenn ein neuer Entwickler hinzukommt oder Sie eine neue Maschine hochfahren. ECOSIRE führt PostgreSQL 17, Redis 7 und Authentik in Docker Compose für die lokale Entwicklung im gesamten Team aus.

Benötigen Sie Hilfe beim Entwurf Ihrer lokalen Entwicklungsinfrastruktur oder beim Containerisieren Ihrer Anwendung für die Produktion? [Entdecken Sie unsere DevOps-Dienste] (/services), um zu sehen, wie wir Ihnen helfen können.

E

Geschrieben von

ECOSIRE Research and Development Team

Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.

Chatten Sie auf WhatsApp