Die Ausführung von Multi-Container-Anwendungen in der Produktion erfordert mehr als eine einfache docker-compose.yml-Datei. Obwohl Docker Compose häufig mit der lokalen Entwicklung assoziiert wird, ist es ein leistungsfähiges Werkzeug für die Bereitstellung von Produktions-Workloads auf Einzelhost- und kleinen Cluster-Umgebungen, wenn es korrekt konfiguriert ist. Diese Anleitung führt Sie durch die Härtung Ihres Docker Compose Setups für die Produktion mit Gesundheitsprüfungen, Neustartrichtlinien, Ressourcenlimits, Geheimnisverwaltung, zentralisierter Protokollierung und Nginx Reverse Proxy mit SSL-Terminierung.

Voraussetzungen

  • Linux-Server mit Docker Engine 24+ und Docker Compose v2 installiert
  • Domainname, der auf die öffentliche IP-Adresse Ihres Servers zeigt
  • Grundlegende Vertrautheit mit Docker-Konzepten (Images, Container, Volumes, Netzwerke)
  • SSL-Zertifikat oder Bereitschaft, Let’s Encrypt mit Certbot zu verwenden
  • SSH-Zugang zu Ihrem Produktionsserver
  • Mindestens 2 GB RAM und 2 CPU-Kerne verfügbar für Ihren Stack

Docker Compose Produktionskonfiguration

Eine produktionsbereite docker-compose.yml unterscheidet sich erheblich von einer Entwicklungskonfiguration. Sie benötigen explizite Neustartrichtlinien, Gesundheitsprüfungen, Ressourcenbeschränkungen und wo möglich schreibgeschützte Dateisysteme.

Neustartrichtlinien

Neustartrichtlinien stellen sicher, dass Ihre Container sich automatisch von Abstürzen, OOM-Kills oder Host-Neustarts erholen:

services:
  web:
    image: myapp:latest
    restart: unless-stopped
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

Verwenden Sie unless-stopped für die meisten Dienste. Dies startet Container nach Abstürzen und Host-Neustarts neu, respektiert aber manuelle Stopps. Für kritische Dienste, die immer laufen müssen, verwenden Sie always. Vermeiden Sie no in der Produktion — es lässt abgestürzte Container tot, bis manuell eingegriffen wird.

Gesundheitsprüfungen

Gesundheitsprüfungen ermöglichen es Docker zu überwachen, ob Ihre Anwendung tatsächlich funktioniert, nicht nur ob der Prozess läuft:

services:
  api:
    image: myapp-api:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Die start_period gibt Ihrer Anwendung Zeit zur Initialisierung, bevor Docker beginnt, fehlgeschlagene Prüfungen zu zählen. Setzen Sie sie höher als die durchschnittliche Startzeit Ihrer Anwendung. Verwenden Sie spezifische Gesundheitsendpunkte, anstatt nur zu prüfen, ob ein Port offen ist — ein Prozess kann auf einem Port lauschen, während er sich in einem fehlerhaften Zustand befindet.

Ressourcenlimits

Ohne Ressourcenlimits kann ein einzelner außer Kontrolle geratener Container den gesamten Systemspeicher verbrauchen und alles zum Absturz bringen:

services:
  api:
    image: myapp-api:latest
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

Setzen Sie limits auf das Maximum, das ein Dienst jemals verwenden sollte, und reservations, um Mindestressourcen zu garantieren. Überwachen Sie Ihre Container eine Woche lang mit docker stats, bevor Sie endgültige Limits festlegen. Zu enge Limits verursachen OOM-Kills und beeinträchtigte Leistung.

Schreibgeschützte Dateisysteme

Minimieren Sie die Angriffsfläche, indem Sie Container mit schreibgeschützten Root-Dateisystemen ausführen:

services:
  api:
    image: myapp-api:latest
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
    volumes:
      - app-data:/app/data

Dies verhindert, dass schädliche Prozesse in das Container-Dateisystem schreiben. Verwenden Sie tmpfs für Verzeichnisse, die temporären Schreibzugriff benötigen, und benannte Volumes für persistente Daten.

Geheimnisse und Umgebungsverwaltung

Fest codierte Zugangsdaten in Ihrer Compose-Datei sind ein Sicherheitsrisiko. Docker Compose unterstützt mehrere Ansätze zur Geheimnisverwaltung.

Verwendung von .env-Dateien

Erstellen Sie eine .env-Datei neben Ihrer Compose-Datei:

# .env - Diese Datei NIEMALS committen
POSTGRES_PASSWORD=your_secure_password_here
API_SECRET_KEY=another_secure_value
REDIS_PASSWORD=redis_password_here

Referenzieren Sie diese in Ihrer Compose-Datei:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Fügen Sie .env sofort zu Ihrer .gitignore hinzu. Für CI/CD-Pipelines injizieren Sie Umgebungsvariablen aus Ihrem Geheimnis-Manager (GitHub Secrets, AWS Secrets Manager oder HashiCorp Vault).

Docker Secrets

Für eine sicherere Handhabung verwenden Sie Docker Secrets, die als Dateien anstatt als Umgebungsvariablen eingebunden werden:

secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

Viele offizielle Docker-Images unterstützen die _FILE-Suffix-Konvention und lesen das Geheimnis aus einer eingebundenen Datei anstatt aus einer Umgebungsvariable. Dies hält Zugangsdaten aus der docker inspect-Ausgabe und den Prozess-Umgebungsauflistungen heraus.

Reverse Proxy und SSL mit Nginx

Anwendungsports direkt freizugeben ist unsicher und unflexibel. Verwenden Sie Nginx als Reverse Proxy mit SSL-Terminierung:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-webroot:/var/www/certbot:ro
      - certbot-certs:/etc/letsencrypt:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  certbot:
    image: certbot/certbot
    volumes:
      - certbot-webroot:/var/www/certbot
      - certbot-certs:/etc/letsencrypt
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"

volumes:
  certbot-webroot:
  certbot-certs:

Die Nginx-Konfiguration sollte auf Ihre internen Dienste weiterleiten:

upstream api_backend {
    server api:8080;
}

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://api_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Beachten Sie, dass nur der Nginx-Dienst Ports zum Host freigibt. Alle anderen Dienste kommunizieren über das interne Docker-Netzwerk, was die Angriffsfläche erheblich reduziert.

Protokollierung und Überwachung

Produktions-Deployments benötigen strukturierte Protokollierung mit Rotation, um das Volllaufen der Festplatte zu verhindern.

JSON-Datei-Protokollierung mit Rotation

services:
  api:
    image: myapp-api:latest
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        tag: "{{.Name}}"

Ohne max-size und max-file wachsen Docker-Logs unbegrenzt und können Ihre Festplatte füllen. Der json-file-Treiber ist der Standard und funktioniert für die meisten Setups gut. Für Multi-Host-Umgebungen sollten Sie Logs an ein zentralisiertes System weiterleiten.

Weiterleitung an externe Systeme

Für Produktions-Observability leiten Sie Logs an einen Aggregationsdienst weiter:

services:
  api:
    image: myapp-api:latest
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://logserver:514"
        tag: "myapp-api"

Alternativen sind die fluentd- und gelf-Treiber für ELK- oder Graylog-Stacks. Unabhängig von Ihrer Wahl sollten Sie immer Log-Rotation auf Docker-Daemon-Ebene als Sicherheitsnetz in /etc/docker/daemon.json konfigurieren:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Grundlegende Überwachung

Fügen Sie Container-Überwachung mit einem leichtgewichtigen Stack hinzu:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    deploy:
      resources:
        limits:
          memory: 256M

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    deploy:
      resources:
        limits:
          memory: 128M

Vergleich: Docker Compose vs Kubernetes vs Docker Swarm

MerkmalDocker ComposeKubernetesDocker Swarm
Setup-KomplexitätNiedrigHochMittel
Multi-Host-UnterstützungNein (Einzelhost)Ja (Cluster)Ja (Cluster)
Automatische SkalierungNeinJa (HPA)Eingeschränkt
Service DiscoveryDNS-basiertDNS + IngressDNS-basiert
GeheimnisverwaltungDateibasiertIntegriert (etcd)Integriert
Rolling UpdatesManuellIntegriertIntegriert
GesundheitsprüfungenJaJa (Liveness/Readiness)Ja
RessourcenlimitsJaJa (Requests/Limits)Ja
LernkurveFlachSteilModerat
Ideal fürEinzelhost, kleine TeamsGroßmaßstab, Multi-TeamKleine Cluster

Docker Compose ist die richtige Wahl, wenn Sie auf einem einzelnen Server oder einer kleinen Anzahl von Servern bereitstellen, Ihr Team klein ist und Sie keine automatische Skalierung benötigen. Viele erfolgreiche SaaS-Produkte laufen vollständig mit Docker Compose in der Produktion.

Praxisszenario

Sie stellen eine Webanwendung bereit, die aus einer Node.js-API, einer PostgreSQL-Datenbank, einem Redis-Cache und einem Nginx Reverse Proxy besteht. Der Server hat 4 GB RAM und 2 CPU-Kerne.

Hier ist die vollständige Produktions-Compose-Datei:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-certs:/etc/letsencrypt:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.25"
          memory: 64M
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"

  api:
    image: myapp-api:1.2.3
    env_file: .env
    secrets:
      - db_password
      - api_key
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 1G
        reservations:
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "0.25"
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

volumes:
  pgdata:
  redisdata:
  certbot-certs:

Bereitstellen mit:

docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs --tail=50

Diese Konfiguration weist ungefähr 1,8 GB der verfügbaren 4 GB zu und lässt Spielraum für das Betriebssystem und Lastspitzen.

Stolperfallen und Sonderfälle

Die Startreihenfolge von Containern garantiert keine Bereitschaft. Die alleinige Verwendung von depends_on wartet nur darauf, dass der Container startet, nicht darauf, dass der Dienst darin bereit ist. Kombinieren Sie depends_on immer mit condition: service_healthy und ordnungsgemäßen Gesundheitsprüfungen.

Verwaiste Container sammeln sich an. Wenn Sie einen Dienst aus Ihrer Compose-Datei entfernen, läuft der alte Container weiter. Führen Sie immer docker compose up -d --remove-orphans aus, um veraltete Container zu bereinigen.

Volume-Berechtigungen verursachen stille Fehler. Wenn Ihr Container als Nicht-Root-Benutzer läuft, ist das eingebundene Volume möglicherweise nicht beschreibbar. Erstellen Sie Volumes vorab mit der richtigen Eigentümerschaft oder verwenden Sie ein Init-Container-Muster.

Docker Compose Rebuilds ziehen standardmäßig keine neuen Images. Das Ausführen von docker compose up -d verwendet gecachte Images wieder. Verwenden Sie docker compose pull && docker compose up -d, um sicherzustellen, dass Sie die neueste Version bereitstellen.

Die Variablenexpansion der .env-Datei kann zu Konflikten führen. Docker Compose interpoliert ${VAR} in der Compose-Datei, bevor es an den Container übergeben wird. Wenn Ihre Anwendungskonfiguration $-Zeichen verwendet, escapen Sie diese mit $$ in der Compose-Datei.

Log-Rotation muss pro Dienst und global konfiguriert werden. Log-Optionen auf Dienstebene überschreiben nicht die Daemon-Standards für andere Container. Konfigurieren Sie beides für vollständige Abdeckung.

Bridge-Netzwerke haben DNS-Auflösungsprobleme mit Unterstrichen. Dienstnamen mit Unterstrichen werden in älteren Docker-Versionen möglicherweise nicht korrekt aufgelöst. Verwenden Sie Bindestriche in Dienstnamen für maximale Kompatibilität.

Zusammenfassung

  • Verwenden Sie restart: unless-stopped und Gesundheitsprüfungen für jeden Produktionsdienst
  • Setzen Sie Speicher- und CPU-Limits mit deploy.resources, um außer Kontrolle geratene Container zu verhindern
  • Verwalten Sie Geheimnisse mit Docker Secrets oder .env-Dateien — codieren Sie niemals Zugangsdaten fest ein
  • Platzieren Sie Nginx als Reverse Proxy vor Anwendungsdiensten mit SSL-Terminierung
  • Konfigurieren Sie Log-Rotation mit max-size und max-file, um das Volllaufen der Festplatte zu verhindern
  • Verwenden Sie depends_on mit condition: service_healthy für zuverlässige Startreihenfolge
  • Führen Sie docker compose up -d --remove-orphans aus, um veraltete Container bei jedem Deployment zu bereinigen
  • Docker Compose ist produktionsbereit für Einzelhost- und Kleinteam-Deployments

Verwandte Artikel