Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 02.06.2026 · 10 min Lesezeit

Docker Compose: Multi-Container-Stacks aufbauen

Mit Docker Compose beschreiben Sie einen Multi-Container-Stack deklarativ in einer compose.yaml und steuern ihn mit wenigen Befehlen. Die Anleitung zeigt Compose-Datei, Kernbefehle, .env-Variablen, einen getesteten Beispiel-Stack aus Web-App und Datenbank sowie den Update-Workflow.

Geprüft am 03.10.2026 · für docker-compose 5.6.0

WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Gestapelte Miniatur-Schiffscontainer auf einem Schreibtisch, per Kabel mit einem kleinen Server verbunden

Mit Docker Compose beschreiben Sie einen Multi-Container-Stack deklarativ in einer compose.yaml (Services, Images, Ports, Volumes, Variablen, Netzwerke, Abhängigkeiten) und starten, stoppen und aktualisieren ihn reproduzierbar mit wenigen Befehlen statt vieler docker run-Aufrufe. Sie lernen Aufbau der Datei, Kernbefehle, .env, einen Beispiel-Stack aus Web-App und Datenbank, Healthchecks und den Update-Workflow. Die Docker-Installation behandeln wir separat.

Voraussetzungen

Auf Ihrem Linux-Server (Debian 12 oder Ubuntu 24.04 LTS) brauchen Sie:

  • Docker Engine inkl. Compose-Plugin: Das Plugin (Compose v2 oder das seit Dezember 2025 erschienene v5) wird als docker compose (mit Leerzeichen) aufgerufen. Das alte docker-compose (v1, Python) ist End-of-Life.
  • Rechte: Mitglied der Gruppe docker oder sudo.
  • Ein Arbeitsverzeichnis je Stack, z. B. /opt/stacks/IHR-STACK/, für compose.yaml und .env.
  • Grundverständnis von Volumes und Netzwerken: hilfreich, nicht zwingend.
  • Hardware für den Beispiel-Stack: 1 CPU-Kern, 1 GB RAM (x86_64 oder ARM64), einige GB für Images. Im Test belegten Postgres und nginx zusammen unter 100 MB RAM; echte Anwendungen brauchen mehr.

Plugin prüfen:

docker compose version
# Beispiel-Ausgabe: Docker Compose version v5.6.0
# (ältere oder Distributionspakete zeigen noch v2.x.x, beide funktionieren hier)

Schritt 1: Aufbau der compose.yaml verstehen

Bevorzugter Dateiname ist compose.yaml; gelesen werden auch compose.yml, docker-compose.yaml und docker-compose.yml. Die wichtigsten Top-Level-Blöcke: services: (Container), volumes: (persistente Datenträger), networks: (eigene Netze).

Wichtig: Das Top-Level-Feld version: ist obsolet. Compose (v2 wie v5) ignoriert es, validiert gegen das aktuelle Schema und warnt; lassen Sie es weg.

Die häufigsten Service-Felder:

FeldBedeutung
imagedas zu verwendende Container-Image, z. B. postgres:16
container_namefester Name statt des generierten; praktisch fürs Wiedererkennen, aber pro Host eindeutig
portsPort-Mapping in der Kurzsyntax [HOST:]CONTAINER[/PROTOCOL], z. B. "8080:80"
volumespersistente Daten als Named Volume name:/pfad oder Bind-Mount /host:/container
environmentUmgebungsvariablen IM Container
restartNeustart-Verhalten, für Self-Hosting meist unless-stopped
depends_onStartreihenfolge und optional Warten auf Bereitschaft
networksZuordnung zu eigenen Netzwerken
healthcheckBereitschaftsprüfung des Dienstes

Minimaler Single-Service:

services:
  web:
    image: nginx:1.27
    container_name: web
    ports:
      - "127.0.0.1:8080:80"
    restart: unless-stopped

restart-Werte richtig setzen

Werte: no (Default), always, on-failure (optional mit Limit, z. B. on-failure:3) und unless-stopped. Für Self-Hosting-Dienste ist unless-stopped am besten: Neustart nach Reboot oder Absturz, aber bewusst angehaltene Container bleiben gestoppt.

Hinweis: no ohne Anführungszeichen gilt in älteren YAML-Parsern als Boolean false. Schreiben Sie es wie die offizielle Referenz als String "no".

Verifizieren: docker compose config --quiet im Projektordner endet ohne Ausgabe. Eine Fehlermeldung nennt Zeile und Feld des YAML-Fehlers.

Schritt 2: Variablen mit der .env-Datei steuern

Geheimnisse und umgebungsabhängige Werte gehören in eine .env neben der compose.yaml. Compose liest sie automatisch und ersetzt damit Platzhalter in der Compose-Datei (Interpolation).

# .env – liegt neben der compose.yaml
POSTGRES_PASSWORD=ein-sehr-langes-zufaelliges-passwort
POSTGRES_DB=appdb
POSTGRES_USER=appuser
WEB_PORT=8080

Zugriff per Interpolations-Syntax:

SyntaxVerhalten
${VAR}setzt den Wert von VAR ein
${VAR:-default}nimmt default, wenn VAR leer oder nicht gesetzt ist
${VAR-default}nimmt default nur, wenn VAR gar nicht gesetzt ist
${VAR:?fehler}bricht mit Fehlermeldung ab, wenn VAR leer oder nicht gesetzt ist
${VAR:+ersatz}nimmt ersatz, wenn VAR gesetzt und nicht leer ist

Nicht verwechseln:

  • Interpolation per .env: ersetzt ${VAR}-Platzhalter in der Compose-Datei.
  • environment: / env_file:: setzt Variablen im laufenden Container.

Eine Variable nur in der .env kommt also nicht im Container an; reichen Sie sie per ${VAR} in environment: durch oder nutzen Sie env_file:. Variablen der Shell-Umgebung haben Vorrang vor der .env. Single-Quotes in der .env sind literal, ohne Interpolation.

Die .env enthält Klartext-Geheimnisse: chmod 600 .env und Eintrag in die .gitignore.

Verifizieren: docker compose config zeigt die eingesetzten Werte statt der ${VAR}-Platzhalter. Fehlt eine mit :? markierte Variable, bricht der Befehl mit Ihrer Fehlermeldung ab.

Schritt 3: Named Volumes und eigene Netzwerke deklarieren

Named Volumes bleiben bei docker compose down erhalten, erst down -v löscht sie. Deklarieren Sie ein Named Volume beim Service und im Top-Level-volumes:-Block, sonst bricht Compose mit volume ... is undefined ab.

services:
  db:
    image: postgres:16
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

Bind-Mounts (/host/pfad:/container/pfad, optional :ro) beziehen relative Pfade auf das Verzeichnis der Compose-Datei. Für Datenbankdaten eignen sich meist Named Volumes, für eigene Konfigdateien Bind-Mounts.

Ein eigenes Netzwerk deklarieren Sie im Top-Level-networks:-Block und weisen es den Services zu. Container im selben Netz erreichen sich über den Servicenamen, die Web-App die Datenbank also als db. Compose legt zwar ohnehin ein Default-Netz an, ein explizites Netz macht die Struktur aber klar und erlaubt feinere Trennung.

services:
  web:
    networks:
      - appnet
  db:
    networks:
      - appnet

networks:
  appnet:

Verifizieren: docker volume ls und docker network ls zeigen nach dem Start Einträge mit dem Projektnamen als Präfix, etwa app_db_data und app_appnet.

Schritt 4: Vollständigen Beispiel-Stack zusammenbauen

Web-App mit PostgreSQL, Named Volume, eigenem Netzwerk, Healthcheck und Variablen aus der .env, als /opt/stacks/app/compose.yaml:

services:
  db:
    image: postgres:16
    container_name: app-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env}
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - appnet
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  web:
    image: nginx:1.27
    container_name: app-web
    restart: unless-stopped
    ports:
      - "127.0.0.1:${WEB_PORT:-8080}:80"
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - appnet

volumes:
  db_data:

networks:
  appnet:

Zusammenspiel:

  • Die Datenbank liegt im Named Volume db_data und überlebt jeden down ohne -v.
  • Geheimnisse und Port kommen per ${VAR} aus der .env; ${POSTGRES_PASSWORD:?...} bricht mit klarer Meldung ab, wenn das Passwort fehlt.
  • Die Web-App erreicht db im Netz appnet.
  • Der Port ist nur an 127.0.0.1 gebunden (Schritt 6).
  • depends_on mit condition: service_healthy startet die Web-App erst nach bestandenem DB-Healthcheck.

Healthcheck kurz erklärt

Ein healthcheck prüft die Bereitschaft: test (Kommando, z. B. ["CMD-SHELL", "..."] oder ["CMD", ...]), interval (Abstand), timeout (max. Dauer), retries (Fehlversuche bis unhealthy) und start_period (Karenzzeit beim Start). Daran erkennt Compose condition: service_healthy.

Verifizieren: Nach docker compose up -d zeigt docker compose ps den Dienst db als (healthy), web startet erst danach. curl -I http://127.0.0.1:8080 liefert HTTP/1.1 200 OK.

Schritt 5: Den Stack mit den Kernbefehlen steuern

Alle Befehle im Verzeichnis der compose.yaml ausführen; compose.yaml und .env werden automatisch gelesen. Kernbefehle:

docker compose up -d        # Stack im Hintergrund (detached) starten/aktualisieren
docker compose ps           # Status der Services: laufend / Health / Ports
docker compose logs -f      # Logs aller Services folgen
docker compose logs -f web  # nur Logs des Service 'web' folgen
docker compose exec db psql -U appuser -d appdb   # Befehl im laufenden Container ausfuehren
docker compose down         # Container + Netzwerke entfernen; Named Volumes bleiben
docker compose down -v      # zusaetzlich Named + anonyme Volumes loeschen (Datenverlust!)

Zum Debuggen:

# Compose-Datei nach Interpolation der .env-Variablen rendern und validieren
docker compose config

# auch Container von nicht mehr definierten Services aufraeumen
docker compose down --remove-orphans

# explizite Pfade fuer Datei und Env (statt aus dem aktuellen Verzeichnis)
docker compose -f /opt/stacks/app/compose.yaml --env-file /opt/stacks/app/.env up -d

docker compose exec führt einen Befehl im laufenden Container aus, etwa eine Datenbank-Shell. Als Benutzer geben Sie den Wert von POSTGRES_USER an; einen Benutzer postgres legt das Image dann nicht an.

Verifizieren: docker compose exec db psql -U appuser -d appdb -c 'select 1' gibt eine Zeile mit 1 zurück. Nach docker compose down listet docker volume ls das Volume weiterhin.

Schritt 6: Ports sicher binden und Reverse Proxy davor setzen

"8080:80" bindet an alle Interfaces (0.0.0.0). Docker bearbeitet die iptables-Regeln direkt und kann so an einer host-basierten Firewall vorbei Ports öffnen. Binden Sie Dienste deshalb standardmäßig an den Loopback:

ports:
  - "127.0.0.1:8080:80"   # nur lokal erreichbar

Öffentlichen Zugriff regelt ein Reverse Proxy mit TLS davor (siehe separate Anleitung); die Container sind dann nicht direkt erreichbar.

Verifizieren: ss -tlnp | grep 8080 zeigt 127.0.0.1:8080 und nicht 0.0.0.0:8080. Von einem anderen Rechner aus ist der Port nicht erreichbar.

Schritt 7: Sauberer Update-Workflow

Updates in zwei Schritten: neue Images ziehen, dann ersetzt Compose nur Container mit geändertem Image oder geänderter Konfiguration:

docker compose pull          # neue Images fuer alle Services ziehen
docker compose up -d         # nur geaenderte Container neu erstellen

# beides in einem Aufruf
docker compose pull && docker compose up -d

# nur ein einzelnes Image aktualisieren
docker compose pull db

Unveränderte Container laufen weiter; die Daten im Named Volume bleiben erhalten. Danach mit docker compose ps und docker compose logs -f den Zustand prüfen.

Verifizieren: docker compose ps zeigt alle Dienste wieder als Up bzw. (healthy), und die Daten in der Datenbank sind nach dem Update noch vorhanden.

Troubleshooting

  • Fehler volume ... is undefined: Named Volume fehlt im Top-Level-volumes:-Block.
  • Variable kommt im Container nicht an: nur in der .env definiert; per ${VAR} in environment: durchreichen oder env_file: (Schritt 2).
  • Web-App erreicht die DB nicht: Die Kurzform depends_on: [db] wartet nur auf den Start. Langform mit condition: service_healthy plus DB-Healthcheck nutzen.
  • Obsolet-Warnung beim Start: Top-Level-version: entfernen.
  • Daten plötzlich weg: Wahrscheinlich lief docker compose down -v, das Named Volumes unwiderruflich löscht. Zum Stoppen docker compose stop oder down ohne -v.
  • Unklar, was Compose sieht: docker compose config zeigt die Datei mit eingesetzten .env-Werten.
  • psql meldet role "postgres" does not exist: psql -U mit dem Wert von POSTGRES_USER aufrufen.
  • Port nicht erreichbar oder zu offen: Bindung prüfen (Schritt 6).

Häufige Fragen

compose.yaml oder docker-compose.yml – welcher Dateiname ist richtig?

compose.yaml; die anderen Namen liest Compose aus Kompatibilität weiter.

Brauche ich noch das version:-Feld am Anfang der Datei?

Nein, es ist obsolet und löst nur eine Warnung aus.

Was ist der Unterschied zwischen docker-compose und docker compose?

docker-compose ist die alte, End-of-Life-v1 in Python, docker compose das aktuelle Plugin (v2, inzwischen v5; die Versionen 3 und 4 wurden übersprungen, um Verwechslungen mit den alten Dateiformat-Versionen zu vermeiden). Tutorials mit Bindestrich können veraltetes Verhalten zeigen.

Löscht docker compose down meine Datenbank?

Nein. docker compose down entfernt Container und Netzwerke, lässt Named Volumes stehen. Erst docker compose down -v (--volumes) löscht Named und anonyme Volumes und damit die Datenbankdaten unwiderruflich.

Wie aktualisiere ich meinen Stack, ohne Daten zu verlieren?

docker compose pull, dann docker compose up -d (Schritt 7).

Wie sorge ich dafür, dass die App erst startet, wenn die Datenbank bereit ist?

Mit depends_on in Langform und condition: service_healthy plus DB-Healthcheck. Weitere Bedingungen: service_started und service_completed_successfully.

Fazit

Docker Compose macht aus einzelnen Containern einen reproduzierbaren, dokumentierten Stack: Named Volumes sichern die Daten, eigene Netzwerke lösen Servicenamen auf, Healthchecks mit depends_on machen den Start robust, pull plus up -d hält alles aktuell. Produktionsreif wird der Stack mit Ports am Loopback, Reverse Proxy mit TLS und geschützter .env.

Weiterführende Anleitungen und Quellen

Quellen: Compose file reference – services (image, ports, volumes, depends_on, healthcheck, restart), Docker Compose CLI reference (up/down/ps/logs/pull/exec), Set environment variables / variable interpolation (.env) und Version and name top-level elements (version ist obsolet).