Docker Compose: Mehrere Services orchestrieren – Der praktische Guide

Docker Compose v2 verstehen und nutzen: compose.yaml schreiben, Services, Volumes, Netzwerke und Umgebungsvariablen konfigurieren – mit realen Beispielen für Web-Apps, Datenbanken und mehr.

9 min Lesezeit

Docker Compose: Mehrere Services orchestrieren – Der praktische Guide

Eine einzelne App braucht selten nur einen Container. Web-Server, Datenbank, Cache, Queue – jeder Service läuft im eigenen Container, und Compose bringt sie alle zusammen. Statt fünf docker run-Befehle mit langen Parametern: eine compose.yaml-Datei, ein docker compose up.

Dieser Guide erklärt, wie Compose funktioniert und wie du reale Setups für Web-Apps, Datenbanken und Entwicklungsumgebungen konfigurierst.


Docker Compose v2: Grundlagen

v2 vs. v1: Der Unterschied

# Alt (v1, deprecated, nicht mehr verwenden):
docker-compose up   # mit Bindestrich, separate Binary

# Neu (v2, integriert in Docker CLI):
docker compose up   # ohne Bindestrich, Plugin
docker compose version
# Docker Compose version v2.40.0

Keine version: in compose.yaml mehr nötig! Ab Compose v2.40 wird das version:-Feld ignoriert. Einfach weglassen.

Installation prüfen

docker compose version
# Docker Compose version v2.40.0

# Falls nicht vorhanden:
sudo apt install docker-compose-plugin   # Ubuntu/Debian
sudo dnf install docker-compose-plugin  # Fedora

Grundlegende Befehle

# Services starten (im Hintergrund)
docker compose up -d

# Status anzeigen
docker compose ps

# Logs anzeigen
docker compose logs
docker compose logs -f              # live verfolgen
docker compose logs -f webapp       # nur ein Service

# Services stoppen (Container bleiben erhalten)
docker compose stop

# Services stoppen und entfernen
docker compose down

# Auch Volumes entfernen (VORSICHT: Datenverlust!)
docker compose down -v

# Service neu starten
docker compose restart webapp

# Einzelnen Service starten
docker compose up -d webapp

# Befehl in laufendem Container ausführen
docker compose exec webapp bash
docker compose exec db mysql -u root -p

# Images neu bauen
docker compose build
docker compose up -d --build   # neu bauen und starten

compose.yaml-Struktur

# compose.yaml (moderner Name; docker-compose.yaml/yml ist ebenfalls gültig)

services:         # Die Container-Definitionen
  service-name:   # Frei wählbarer Name (wird im Netzwerk verwendet)
    image: ...    # Fertig-Image aus Registry
    build: ...    # Oder: eigenes Dockerfile
    ports: ...
    volumes: ...
    environment: ...
    depends_on: ...

volumes:          # Benannte Volumes (persistente Daten)
  daten:

networks:         # Eigene Netzwerke
  intern:

Minimales Beispiel

services:
  web:
    image: nginx:1.28-alpine
    ports:
      - "80:80"
docker compose up -d
# Container läuft, Port 80 erreichbar

Services konfigurieren

image vs. build

services:
  # Fertiges Image
  db:
    image: mariadb:11.6

  # Eigenes Dockerfile im aktuellen Verzeichnis
  webapp:
    build: .

  # Eigenes Dockerfile an anderem Pfad
  api:
    build:
      context: ./backend        # Verzeichnis mit Dockerfile
      dockerfile: Dockerfile.prod  # spezifisches Dockerfile
      args:
        NODE_VERSION: "22"       # Build-Argumente

Ports

services:
  web:
    image: nginx:1.28-alpine
    ports:
      # HOST:CONTAINER
      - "80:80"
      - "443:443"
      # Nur auf localhost exponieren (nicht von außen erreichbar)
      - "127.0.0.1:8080:80"
      # Zufälligen Host-Port verwenden
      - "8080"

Restart-Policies

services:
  webapp:
    image: myapp:latest
    restart: unless-stopped   # Standard für Produktions-Services
    # Optionen:
    # no          : Kein Neustart (Standard)
    # always      : Immer neu starten
    # on-failure  : Nur bei Fehler (Exit-Code != 0)
    # unless-stopped : Immer außer bei manuellem Stop

Resource-Limits

services:
  webapp:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: "0.5"       # Max 50% einer CPU
          memory: 512M      # Max 512 MB RAM
        reservations:
          cpus: "0.1"
          memory: 128M

Health-Checks

services:
  db:
    image: mariadb:11.6
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
      interval: 10s       # Wie oft prüfen?
      timeout: 5s         # Timeout pro Prüfung
      retries: 5          # Wie oft versuchen vor "unhealthy"?
      start_period: 30s   # Wartezeit beim Start

  webapp:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy   # Warten bis db healthy ist

depends_on

services:
  webapp:
    depends_on:
      - db          # Einfach: db startet zuerst
      - redis
      db:
        condition: service_healthy   # Wartet bis healthy

Volumes: Persistente Daten

Ohne Volumes gehen Daten beim docker compose down verloren!

Benannte Volumes (empfohlen für Datenbanken)

services:
  db:
    image: mariadb:11.6
    volumes:
      - db-daten:/var/lib/mysql     # benanntes Volume

volumes:
  db-daten:     # Compose verwaltet das Volume
# Volumes anzeigen
docker volume ls
docker volume inspect projektname_db-daten

Bind Mounts (für Entwicklung)

services:
  webapp:
    build: .
    volumes:
      # Lokales Verzeichnis → Container (für Hot-Reload)
      - ./src:/app/src
      - ./config:/app/config:ro    # :ro = read-only

      # Einzelne Datei
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

tmpfs-Mounts (RAM, kein Disk)

services:
  webapp:
    image: myapp
    tmpfs:
      - /tmp
      - /run

Netzwerke: Services verbinden

Compose erstellt automatisch ein Standard-Netzwerk. Services im selben Netzwerk können sich über den Service-Namen ansprechen.

services:
  webapp:
    image: myapp
    # Kann "db" und "redis" direkt über Namen ansprechen

  db:
    image: mariadb:11.6

  redis:
    image: redis:7-alpine
# In der App: Verbindung über Service-Name (kein IP nötig!)
db_connection = "mysql://db:3306/meinedatenbank"
redis_connection = "redis://redis:6379"

Eigene Netzwerke

services:
  nginx:
    networks:
      - frontend    # Zugriff von außen
      - backend     # Interne Kommunikation

  webapp:
    networks:
      - backend     # Nur intern

  db:
    networks:
      - backend     # Nur intern

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # Kein Internetzugang

Externes Netzwerk (über andere Compose-Stacks)

networks:
  extern-netz:
    external: true   # Muss vorher existieren
    name: mein-netz

Umgebungsvariablen und .env

.env-Datei (automatisch geladen)

# .env im selben Verzeichnis wie compose.yaml
nano .env
# .env - NIEMALS in Git einchecken!
MARIADB_ROOT_PASSWORD=sicheres-passwort-123
MARIADB_DATABASE=meineapp
MARIADB_USER=appuser
MARIADB_PASSWORD=app-passwort-456
APP_SECRET_KEY=supersecretkey
APP_DEBUG=false
# compose.yaml: Variablen referenzieren
services:
  db:
    image: mariadb:11.6
    environment:
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MARIADB_DATABASE: ${MARIADB_DATABASE}
      MARIADB_USER: ${MARIADB_USER}
      MARIADB_PASSWORD: ${MARIADB_PASSWORD}

env_file: Eigene Env-Datei

services:
  webapp:
    env_file:
      - .env            # Standard .env
      - .env.local      # Lokale Überschreibungen (optional)

Unterschied: environment vs. env_file

services:
  webapp:
    # Direkt in compose.yaml (für Nicht-Secrets):
    environment:
      NODE_ENV: production
      PORT: "3000"
      # Variable aus .env übernehmen:
      DATABASE_URL: ${DATABASE_URL}

    # Aus Datei (für Secrets empfohlen):
    env_file:
      - .env

Praxisbeispiele

Beispiel 1: Laravel + MariaDB + Redis + Nginx

# compose.yaml für Laravel-App

services:
  nginx:
    image: nginx:1.28-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - app-daten:/var/www/html:ro
    depends_on:
      - php-fpm
    restart: unless-stopped

  php-fpm:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - app-daten:/var/www/html
      - ./.env:/var/www/html/.env:ro
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  db:
    image: mariadb:11.6
    volumes:
      - db-daten:/var/lib/mysql
    environment:
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MARIADB_DATABASE: ${MARIADB_DATABASE}
      MARIADB_USER: ${MARIADB_USER}
      MARIADB_PASSWORD: ${MARIADB_PASSWORD}
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis-daten:/data
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    restart: unless-stopped

volumes:
  app-daten:
  db-daten:
  redis-daten:

Beispiel 2: Entwicklungsumgebung (Node.js + PostgreSQL)

# compose.yaml für Entwicklung (mit Hot-Reload)

services:
  api:
    build:
      context: .
      target: development    # Multi-Stage Dockerfile
    ports:
      - "3000:3000"
      - "9229:9229"          # Node.js Debugger
    volumes:
      - .:/app               # Lokaler Code im Container
      - /app/node_modules    # node_modules NICHT überschreiben
    environment:
      NODE_ENV: development
      DATABASE_URL: postgresql://pguser:pgpass@db:5432/devdb
    command: npm run dev     # Hot-Reload-Befehl
    depends_on:
      - db

  db:
    image: postgres:17-alpine
    ports:
      - "5432:5432"          # Direkt zugänglich für DB-Tools
    volumes:
      - pg-daten:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    environment:
      POSTGRES_DB: devdb
      POSTGRES_USER: pguser
      POSTGRES_PASSWORD: pgpass

  adminer:
    image: adminer:latest    # Web-UI für DB (nur Entwicklung)
    ports:
      - "8080:8080"

volumes:
  pg-daten:

Beispiel 3: Monitoring-Stack (Grafana + Prometheus)

services:
  prometheus:
    image: prom/prometheus:v3.1.0
    ports:
      - "127.0.0.1:9090:9090"   # Nur localhost
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-daten:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'
    restart: unless-stopped

  grafana:
    image: grafana/grafana:11.4.0
    ports:
      - "3000:3000"
    volumes:
      - grafana-daten:/var/lib/grafana
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
      GF_USERS_ALLOW_SIGN_UP: "false"
    depends_on:
      - prometheus
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter:v1.8.2
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
    network_mode: host        # Direkter Host-Netzwerk-Zugriff
    pid: host
    restart: unless-stopped

volumes:
  prometheus-daten:
  grafana-daten:

Profiles und Overrides

Profiles: Optionale Services

services:
  webapp:
    image: myapp
    # Startet immer

  db:
    image: mariadb:11.6
    # Startet immer

  adminer:
    image: adminer
    profiles:
      - dev          # Startet NUR mit --profile dev
# Standard: webapp + db
docker compose up -d

# Mit adminer (nur für Entwicklung)
docker compose --profile dev up -d

Overrides: dev vs. prod

# compose.yaml (Basis):
services:
  webapp:
    image: myapp:${TAG:-latest}
    restart: unless-stopped
# compose.override.yaml (wird automatisch geladen, für Entwicklung):
services:
  webapp:
    build: .
    volumes:
      - .:/app          # Hot-Reload für Entwicklung
    environment:
      DEBUG: "true"
# compose.prod.yaml (explizit angeben, für Produktion):
services:
  webapp:
    deploy:
      resources:
        limits:
          memory: 512M
# Entwicklung (automatisch compose.yaml + compose.override.yaml)
docker compose up -d

# Produktion (explizit angeben)
docker compose -f compose.yaml -f compose.prod.yaml up -d

Häufige Probleme

Error: No such service

# Tipp-Fehler im Service-Namen?
docker compose ps    # Alle Services anzeigen
docker compose config  # Gesamte Config anzeigen

# Compose-Datei im falschen Verzeichnis?
ls compose.yaml docker-compose.yaml docker-compose.yml

Service startet immer neu (Restarting)

# Logs prüfen
docker compose logs webapp

# Exit-Code prüfen
docker compose ps
# STATUS: Restarting (1) 3 seconds ago   ← Exit-Code 1 = Fehler

# Container interaktiv starten (umgeht restart-policy)
docker compose run --rm webapp bash

Port bereits belegt

# Fehler: Bind for 0.0.0.0:80 failed: port is already allocated

# Wer nutzt Port 80?
sudo ss -tlnp | grep :80
sudo lsof -i :80

# Prozess stoppen oder anderen Port wählen

Volume-Berechtigungsprobleme

# Container läuft als anderer User als Volume-Besitzer
docker compose exec webapp id
# uid=1000(node) ...

ls -la $(docker volume inspect projektname_app-daten --format '{{.Mountpoint}}')

# Korrektur im Dockerfile:
# RUN chown -R node:node /app
# USER node

depends_on wartet nicht richtig

# depends_on wartet nur auf "started", nicht auf "ready"
# Lösung: healthcheck + condition: service_healthy

services:
  db:
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping"]
      ...
  webapp:
    depends_on:
      db:
        condition: service_healthy

Fazit

Docker Compose macht Multi-Service-Setups reproduzierbar und portabel. Die wichtigsten Punkte:

  • Kein version: mehr in compose.yaml (ab v2.40)
  • Services sprechen sich über Namen an (db, redis) – kein Hardcoding von IPs
  • Benannte Volumes für persistente Daten (Datenbanken, Uploads)
  • .env-Datei für Secrets – nie in Git einchecken
  • Overrides für dev/prod trennen Entwicklungs- von Produktionsconfig
  • depends_on mit condition: service_healthy für zuverlässige Startreihenfolge

War dieser Artikel hilfreich?