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

Docker Compose: Multi-Container-Stacks aufbauen

Mit Docker Compose beschreibst du einen kompletten Multi-Container-Stack deklarativ in einer compose.yaml und steuerst ihn mit wenigen Befehlen. Diese Anleitung zeigt dir Compose-Datei, Kernbefehle, .env-Variablen, einen Beispiel-Stack aus Web-App und Datenbank sowie den Update-Workflow.

Infografik zu Docker Compose für den Aufbau von Multi-Container-Stacks, mit docker-compose.yml, mehreren Services, Netzwerken, Volumes, Abhängigkeiten und gemeinsam gestarteten Containern.

Mit Docker Compose beschreibst du einen kompletten Multi-Container-Stack deklarativ in einer einzigen Datei und startest, stoppst und aktualisierst ihn mit wenigen Befehlen. Statt mehrere docker run-Aufrufe von Hand zu jonglieren, legst du Services, Images, Ports, Volumes, Variablen, Netzwerke und Abhängigkeiten in einer compose.yaml fest und reproduzierst den Stack jederzeit identisch. Diese Anleitung richtet sich an Admins im Mittelstand, die Self-Hosting betreiben oder Dienste sauber containerisieren wollen. Du lernst den Aufbau der Compose-Datei, die wichtigsten Befehle, die .env-Datei, einen vollständigen Beispiel-Stack aus Web-App und Datenbank, Healthchecks und einen sauberen Update-Workflow. Die Docker-Installation selbst ist nicht Thema dieser Anleitung – die behandeln wir separat.

Kurzfassung: Lege eine compose.yaml an (ohne obsoletes version:-Feld), definiere unter services: deine Container mit image, ports, volumes, environment, restart, depends_on und networks. Named Volumes und eigene Netzwerke deklarierst du zusätzlich in den Top-Level-Blöcken volumes: und networks:. Geheimnisse und Konfig legst du in eine .env-Datei neben der Compose-Datei und reichst sie per ${VAR} durch. Gestartet wird mit docker compose up -d, Status mit docker compose ps, Logs mit docker compose logs -f. Aktualisiert wird sauber mit docker compose pull gefolgt von docker compose up -d. docker compose down entfernt Container und Netze, lässt Named Volumes aber stehen – erst down -v löscht die Daten.

Voraussetzungen

Bevor du loslegst, sollten diese Grundlagen auf deinem Linux-Server (Debian 12 oder Ubuntu 24.04 LTS) stehen:

  1. Docker Engine inkl. Compose-Plugin installiert: Compose v2 ist das offizielle Plugin und wird als docker compose (mit Leerzeichen) aufgerufen. Das alte docker-compose (mit Bindestrich, v1, Python) ist End-of-Life – nutze es nicht. Die Installation behandelt unsere separate Anleitung.
  2. Shell-Zugang mit ausreichenden Rechten: entweder als Mitglied der Gruppe docker oder per sudo.
  3. Ein Arbeitsverzeichnis je Stack: üblich ist ein Pfad wie /opt/stacks/DEIN-STACK/. Dorthin gehören compose.yaml und .env.
  4. Grundverständnis von Volumes und Netzwerken: hilfreich, aber nicht zwingend – die Kernkonzepte streifen wir hier.

Prüfe zunächst, dass das Plugin verfügbar ist:

docker compose version
# Beispiel-Ausgabe: Docker Compose version v2.x.x

Schritt 1: Aufbau der compose.yaml verstehen

Die compose.yaml ist eine YAML-Datei, in der du deinen Stack deklarativ beschreibst. Der bevorzugte Dateiname ist compose.yaml; gelesen werden auch compose.yml, docker-compose.yaml und docker-compose.yml. Die drei wichtigsten Top-Level-Blöcke sind services: (deine Container), volumes: (persistente Datenträger) und networks: (eigene Netze).

Wichtig: Das früher übliche Top-Level-Feld version: ist obsolet. Compose v2 ignoriert es, validiert immer gegen das aktuelle Schema und gibt bei Verwendung eine Warnung aus. Lass es ersatzlos weg.

Diese Felder eines Service brauchst du am häufigsten:

FeldBedeutung

image

das zu verwendende Container-Image, z. B. postgres:16

container_name

fester Name statt des generierten; praktisch fürs Wiedererkennen, aber pro Host eindeutig

ports

Port-Mapping in der Kurzsyntax [HOST:]CONTAINER[/PROTOCOL], z. B. "8080:80"

volumes

persistente Daten als Named Volume name:/pfad oder Bind-Mount /host:/container

environment

Umgebungsvariablen IM Container

restart

Neustart-Verhalten, für Self-Hosting meist unless-stopped

depends_on

Startreihenfolge und optional Warten auf Bereitschaft

networks

Zuordnung zu eigenen Netzwerken

healthcheck

Bereitschaftsprüfung des Dienstes

Ein minimaler Single-Service sieht so aus:

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

restart-Werte richtig setzen

Die gängigen Werte sind no (Default), always, on-failure (optional mit Limit, z. B. on-failure:3) und unless-stopped. Für dauerhaft laufende Self-Hosting-Dienste ist unless-stopped die beste Wahl: Der Container startet nach Neustart oder Absturz wieder, bleibt aber gestoppt, wenn du ihn bewusst angehalten hast.

Achtung: restart: no ohne Anführungszeichen wird in YAML als Boolean false interpretiert. Schreibe es immer als String "no".

Schritt 2: Variablen mit der .env-Datei steuern

Geheimnisse wie Datenbank-Passwörter und umgebungsabhängige Werte gehören nicht hart in die Compose-Datei. Lege sie in eine .env-Datei, die standardmäßig neben der compose.yaml liegt. Compose liest sie automatisch und ersetzt damit Platzhalter in der Compose-Datei (sogenannte Interpolation), bevor Docker sie verarbeitet.

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

In der Compose-Datei greifst du mit der Interpolations-Syntax darauf zu:

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

Wichtig ist der Unterschied zwischen zwei Dingen, die leicht verwechselt werden:

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

Eine Variable nur in der .env zu definieren reicht also nicht, damit sie im Container ankommt – du musst sie über ${VAR} in einem environment:-Eintrag durchreichen oder eine env_file: angeben. Zur Precedence: Variablen aus der echten Shell-Umgebung haben Vorrang vor Werten aus der .env. Single-Quotes in der .env sind literal, dort findet keine Interpolation statt.

Die .env enthält Klartext-Geheimnisse. Schränke die Dateirechte ein (chmod 600 .env) und nimm sie in die .gitignore auf, damit sie nicht in der Versionsverwaltung landet.

Schritt 3: Named Volumes und eigene Netzwerke deklarieren

Damit Daten einen down überleben, brauchst du Named Volumes. Sie werden von Docker verwaltet und bleiben bei docker compose down erhalten – erst down -v löscht sie. Ein Named Volume musst du sowohl beim Service als auch zusätzlich im Top-Level-volumes:-Block deklarieren, sonst bricht Compose mit volume ... is undefined ab.

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

volumes:
  db_data:

Alternativ gibt es Bind-Mounts (/host/pfad:/container/pfad, optional mit :ro für read-only). Relative Quellpfade beziehen sich auf das Verzeichnis der Compose-Datei. Für Datenbankdaten sind Named Volumes meist die bessere Wahl, für eigene Konfigdateien Bind-Mounts.

Ein eigenes Netzwerk deklarierst du im Top-Level-networks:-Block und weist es den Services zu. Container im selben Netz erreichen sich über den Servicenamen als DNS-Hostname – die Web-App spricht die Datenbank also als db an, ganz ohne IP-Adressen. Compose legt ohnehin automatisch ein projekteigenes Default-Netz an; ein explizites Netz macht die Struktur aber klar und erlaubt feinere Trennung.

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

networks:
  appnet:

Schritt 4: Vollständigen Beispiel-Stack zusammenbauen

Jetzt setzt du alles zu einem produktionsnahen Stack zusammen: eine Web-App, die eine PostgreSQL-Datenbank nutzt, mit Named Volume, eigenem Netzwerk, Healthcheck und Variablen aus der .env. Lege die folgende Datei als /opt/stacks/app/compose.yaml an:

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:

Was hier zusammenspielt:

  1. Der Datenbank-Container speichert seine Daten im Named Volume db_data und überlebt damit jeden down ohne -v.
  2. Alle Geheimnisse und der Port kommen per ${VAR} aus der .env; ${POSTGRES_PASSWORD:?...} bricht mit klarer Meldung ab, falls das Passwort fehlt.
  3. Die Web-App erreicht die Datenbank über den Hostnamen db im gemeinsamen Netz appnet.
  4. Der Port ist mit 127.0.0.1 bewusst nur an den Loopback gebunden – siehe Schritt 6.
  5. Dank depends_on mit condition: service_healthy startet die Web-App erst, wenn der Healthcheck der Datenbank besteht.

Healthcheck kurz erklärt

Ein healthcheck prüft, ob ein Dienst wirklich bereit ist. Die wichtigsten Felder: test (das Kommando, z. B. ["CMD-SHELL", "..."] oder ["CMD", ...]), interval (Prüfabstand), timeout (max. Dauer pro Prüfung), retries (Fehlversuche bis unhealthy) und start_period (Karenzzeit beim Start, in der Fehlversuche noch nicht zählen). So weiß Compose, wann condition: service_healthy erfüllt ist.

Schritt 5: Den Stack mit den Kernbefehlen steuern

Alle Befehle führst du im Verzeichnis der compose.yaml aus; Compose liest compose.yaml und .env dann automatisch. Das sind die 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 postgres   # Befehl im laufenden Container ausfuehren
docker compose down         # Container + Netzwerke entfernen; Named Volumes bleiben
docker compose down -v      # zusaetzlich Named + anonyme Volumes loeschen (Datenverlust!)

Drei Befehle, die du oft beim Debuggen brauchst:

# 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 bereits laufenden Container aus und startet keinen neuen. Das ist der richtige Weg, um etwa eine Datenbank-Shell zu öffnen oder schnell etwas zu prüfen.

Schritt 6: Ports sicher binden und Reverse Proxy davor setzen

Ein Port-Mapping wie "8080:80" bindet an alle Interfaces (0.0.0.0) und ist damit potenziell öffentlich erreichbar. Brisant: Docker bearbeitet die iptables-Regeln direkt und kann so an einer host-basierten Firewall vorbei Ports öffnen. Binde Dienste deshalb standardmäßig an den Loopback:

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

Den öffentlichen Zugriff regelst du dann sauber über einen Reverse Proxy mit TLS davor – so terminierst du HTTPS zentral und exponierst die Container nicht direkt. Wie das geht, zeigt unsere separate Anleitung zum Reverse Proxy.

Schritt 7: Sauberer Update-Workflow

Images aktualisierst du nicht durch einen kompletten Neustart des Stacks, sondern in zwei Schritten. Zuerst ziehst du die neuen Images, dann lässt du Compose nur die Container ersetzen, deren Image oder Konfiguration sich geändert hat:

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 dabei weiter – es gibt keinen vollen Stack-Neustart. Da die Datenbankdaten im Named Volume liegen, bleiben sie über das Update hinweg erhalten. Nach dem Update lohnt ein Blick auf docker compose ps und docker compose logs -f, um den gesunden Zustand zu bestätigen.

Troubleshooting

  1. Fehler volume ... is undefined: Du nutzt ein Named Volume im Service, hast es aber nicht im Top-Level-volumes:-Block deklariert. Trage es dort nach.
  2. Variable kommt im Container nicht an: Eine Definition allein in der .env setzt sie nur als Platzhalter für die Compose-Datei, nicht im Container. Reiche sie per ${VAR} in environment: durch oder nutze env_file:.
  3. Web-App startet, kann die DB aber nicht erreichen: Die Kurzform depends_on: [db] wartet nur auf den Container-Start, nicht auf Bereitschaft. Nutze die Langform mit condition: service_healthy und einen Healthcheck im DB-Service.
  4. Obsolet-Warnung beim Start: Ein Top-Level-version: löst eine Warnung aus. Entferne die Zeile ersatzlos.
  5. Daten plötzlich weg: Wahrscheinlich wurde docker compose down -v ausgeführt – das löscht Named Volumes unwiderruflich. Für einen reinen Stopp nutze docker compose stop oder down ohne -v.
  6. Unklar, was Compose tatsächlich sieht: docker compose config rendert die Datei nach Einsetzen aller .env-Variablen und zeigt, welche Werte real verwendet werden.
  7. Port nicht erreichbar oder umgekehrt zu offen: Prüfe die Bindung. "8080:80" ist an allen Interfaces offen, "127.0.0.1:8080:80" nur lokal.

Häufige Fragen

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

Der bevorzugte und empfohlene Name ist compose.yaml. Aus Kompatibilität liest Compose v2 auch compose.yml, docker-compose.yaml und docker-compose.yml. Für neue Projekte nimm compose.yaml.

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

Nein. Das Top-Level-version:-Feld ist obsolet. Compose v2 ignoriert es, validiert immer gegen das aktuelle Schema und warnt bei Verwendung. Lass es weg.

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

docker-compose (mit Bindestrich) ist die alte v1 in Python und End-of-Life. docker compose (mit Leerzeichen) ist das aktuelle, offizielle v2-Plugin. Nutze immer die Variante mit Leerzeichen; Tutorials mit Bindestrich können veraltetes Verhalten zeigen.

Löscht docker compose down meine Datenbank?

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

Wie aktualisiere ich meinen Stack, ohne Daten zu verlieren?

Mit docker compose pull gefolgt von docker compose up -d. Compose ersetzt nur die Container mit geändertem Image oder geänderter Konfiguration; die Daten in den Named Volumes bleiben erhalten.

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

Über die Langform von depends_on mit condition: service_healthy kombiniert mit einem Healthcheck im Datenbank-Service. Die Kurzform garantiert nur die Startreihenfolge, nicht die Bereitschaft. Weitere Bedingungen sind service_started und service_completed_successfully.

Fazit

Docker Compose macht aus einem Bündel einzelner Container einen reproduzierbaren, dokumentierten Stack. Du beschreibst Services, Images, Ports, Volumes, Variablen, Netzwerke und Abhängigkeiten einmal in der compose.yaml, lagerst Geheimnisse in die .env aus und steuerst alles mit einer Handvoll Befehle. Mit Named Volumes überleben deine Daten jeden down, mit eigenen Netzwerken erreichen sich Container über den Servicenamen, mit Healthchecks und depends_on wird die Startreihenfolge robust, und der Zweischritt aus pull und up -d hält den Stack sauber aktuell. Damit hast du das Fundament für stabiles Self-Hosting – binde Ports an den Loopback, setze einen Reverse Proxy mit TLS davor und schütze deine .env, dann ist der Stack auch produktionsreif.

Weiterführende Anleitungen und Quellen

  1. Docker installieren: Engine und Compose-Plugin einrichten – die Basis, die du für diese Anleitung brauchst.
  2. Docker-Netzwerke und Volumes verstehen – tiefer in die Konzepte hinter Volumes und Netzwerken.
  3. Nginx als Reverse Proxy mit TLS einrichten – den öffentlichen Zugriff sauber vor deinen Stack setzen.
  4. Alle Docker-Anleitungen in der Kategorie Docker – weitere Container-Themen im Überblick.

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