Das Problem, das Compose löst

Eine echte Anwendung ist selten nur ein Container. Eine Web-API braucht eine Datenbank, die Datenbank braucht ein Volume, damit ihre Daten einen Neustart überstehen, und dann kommen noch ein Redis-Cache und ein Hintergrund-Worker dazu. Das sind vier docker run-Befehle, jeder mit eigenen Flags für Ports, Volumes und Umgebungsvariablen, dazu ein gemeinsames Netzwerk. In der richtigen Reihenfolge. Jedes Mal, wenn Sie sich an die Arbeit setzen.

Docker Compose ersetzt diese Befehle durch eine Datei und einen Befehl. Sie beschreiben die Container und ihre Verbindungen in einer Datei namens compose.yaml, und docker compose up startet sie alle. Wenn Sie mit Images und docker run noch nicht gearbeitet haben, lesen Sie zuerst was Docker ist und wie Container funktionieren.

Was die Compose-Datei beschreibt

Eine Compose-Datei hat eine Handvoll Schlüssel auf oberster Ebene. Einen davon verwenden Sie immer: services – jeder Service ist ein Container, den Compose ausführt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

Zwei Container. api wird aus dem Dockerfile im aktuellen Verzeichnis gebaut und gibt Port 8000 frei. db führt das offizielle Image postgres:18 aus und legt seine Daten in einem benannten Volume ab, sodass die Daten erhalten bleiben, wenn der Container neu erstellt wird.

Zwei Details erledigen hier die eigentliche Arbeit:

  • api erreicht db über den Hostnamen db. Compose legt jeden Service in ein gemeinsames Netzwerk und registriert jeden Servicenamen als DNS-Namen. Keine IP-Adressen, kein altes --link. DATABASE_URL zeigt auf db:5432, und Compose löst den Namen auf.
  • depends_on mit condition: service_healthy hält api zurück, bis Postgres wirklich antwortet. Ein einfaches depends_on wartet nur darauf, dass der Container startet, nicht darauf, dass der Datenbankprozess darin Verbindungen annimmt – dieser Unterschied ist der häufigste Grund für ein “connection refused” beim ersten Start.

Die drei Schlüssel auf oberster Ebene: services, volumes, networks

Drei Schlüssel auf oberster Ebene entsprechen Docker-Konzepten, die Sie schon kennen:

SchlüsselWas er definiertVon Hand mit
servicesdie auszuführenden Containerdocker run
volumesbenannte Volumes für Daten, die bestehen bleiben müssendocker volume create
networksNetzwerke zwischen Servicesdocker network create

networks deklarieren Sie selten. Compose erstellt ein Netzwerk pro Projekt und hängt jeden Service daran an. Deshalb brauchte das Postgres-Beispiel keine Netzwerkkonfiguration. Deklarieren Sie Netzwerke nur dann explizit, wenn Sie verhindern wollen, dass Servicegruppen einander erreichen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  api:
    build: .
    networks: [frontend, backend]
  db:
    image: postgres:18
    networks: [backend]        # not reachable from frontend
  proxy:
    image: caddy:2
    networks: [frontend]

networks:
  frontend:
  backend:

Die Befehle, die Sie wirklich verwenden

Diese Befehle führen Sie in dem Verzeichnis aus, in dem compose.yaml liegt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Alles starten, Logs im Terminal
docker compose up

# Im Hintergrund starten
docker compose up -d

# Images mit build-Abschnitt neu bauen, dann starten
docker compose up --build

# Container und Netzwerke stoppen und entfernen (benannte Volumes bleiben)
docker compose down

# Dasselbe, und auch benannte Volumes löschen
docker compose down -v

# Was für dieses Projekt läuft
docker compose ps

# Logs eines Service verfolgen
docker compose logs -f api

# Einmaliger Befehl in einem frischen Container
docker compose run --rm api python manage.py migrate

# Shell in einem bereits laufenden Container
docker compose exec api bash

up und down sind das Paar, das Sie am häufigsten tippen. up können Sie gefahrlos erneut aufrufen: Nach einer Änderung an der Datei erstellt es nur die Services neu, deren Konfiguration sich geändert hat, und lässt den Rest in Ruhe.

Was docker compose up beim ersten Start ausgibt

Führen Sie das Postgres-Beispiel von oben aus, und Sie bekommen ungefähr das:

1
2
3
4
5
[+] Running 4/4
 ✔ Network app_default    Created
 ✔ Volume "app_db-data"   Created
 ✔ Container app-db-1      Healthy
 ✔ Container app-api-1     Started

Jedem Namen stellt Compose den Projektnamen voran (standardmäßig den des Verzeichnisses) und hängt eine Nummer an, weil Compose auch mehrere Repliken eines Service ausführen kann. app-db-1 meldet Healthy, bevor app-api-1 startet – hier wirkt condition: service_healthy.

Wie Sie Passwörter aus der Compose-Datei heraushalten

Compose liest eine Datei namens .env im Projektverzeichnis und ersetzt ${VAR}-Referenzen in der Compose-Datei:

1
2
3
4
5
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
1
2
# .env  — add this file to .gitignore
DB_PASSWORD=secret

Halten Sie .env aus der Versionsverwaltung heraus und committen Sie stattdessen eine .env.example mit leeren oder Dummy-Werten. In einer echten Deployment-Umgebung verwenden Sie für alles wirklich Sensible Docker Secrets statt Umgebungsvariablen.

Optionale Services mit Profiles

Nicht jeder Service muss jedes Mal starten. Ein profiles-Eintrag hält einen Service inaktiv, bis Sie ihn anfordern:

1
2
3
4
5
6
7
8
9
services:
  api:
    build: .
  db:
    image: postgres:18
  seed:
    build: .
    command: python manage.py seed_demo_data
    profiles: [tools]

docker compose up startet api und db und ignoriert seed komplett. docker compose --profile tools run --rm seed führt den Seeder aus, wenn Sie ihn anfordern. Verwenden Sie Profiles für alles Einmalige: Seeder, Migrationen, eine Debug-Shell, einen Lastgenerator, den Sie nur für einen Test brauchen.

Compose v2 vs das alte docker-compose

Wenn eine Anleitung Ihnen sagt, Sie sollen docker-compose mit Bindestrich ausführen, stammt sie von vor 2023. Das war v1, in Python geschrieben, inzwischen am Ende ihres Lebenszyklus. Das aktuelle Werkzeug ist docker compose als Unterbefehl der Docker-CLI; es ist in Docker Desktop und im Compose-Plugin-Paket für Docker Engine enthalten.

Ältere Dateien haben oft noch diese Zeile am Anfang:

1
version: "3.8"   # delete it

Der Schlüssel version hat in Compose v2 keine Wirkung. Compose validiert gegen die aktuelle Compose Specification und warnt, wenn der Schlüssel vorhanden ist. Entfernen Sie ihn, und die Warnung verschwindet.

Auch der Standard-Dateiname hat sich geändert: compose.yaml ist der aktuelle, docker-compose.yml funktioniert weiterhin, und Compose sucht nach beiden.

Live-Reload während der Entwicklung

docker compose watch (Compose 2.22 und neuer) aktualisiert Container, während Sie am Code arbeiten. Fügen Sie dem Service einen develop-Block hinzu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  api:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt

sync kopiert geänderte Dateien direkt in den laufenden Container, sodass eine Code-Änderung ohne Rebuild sichtbar wird. rebuild löst einen vollständigen Image-Rebuild aus, wenn sich eine Datei ändert, die den Build betrifft, etwa eine Lock-Datei für Abhängigkeiten. Starten Sie es mit docker compose watch oder hängen Sie --watch an up an.

Wann Compose das falsche Werkzeug ist

Compose führt Container auf einer Maschine aus. Das ist die Grenze, und das meiste, was Leute in Compose vermissen, liegt auf der anderen Seite.

Es passt zu lokaler Entwicklung, automatisierten Tests in der CI und kleinen Deployments auf einem einzelnen Host. Es kann keine Container über mehrere Server verteilen, keinen ersetzen, wenn ein Knoten ausfällt, keine schrittweisen Rollouts an Healthchecks knüpfen und nicht automatisch skalieren.

Das sind Aufgaben eines Orchestrators: Kubernetes, oder Docker Swarm, wenn Sie etwas Leichteres wollen. Die beiden stehen nicht in Konkurrenz. Eine Compose-Datei ist oft der Entwurf, der später zu einem Satz Kubernetes-Manifeste wird, und viele Teams nutzen Compose lokal weiter, lange nachdem die Produktion auf einen Cluster umgezogen ist.

Podman-Nutzer haben ebenfalls einen kompatiblen Weg: podman compose und podman-compose lesen dasselbe Dateiformat, mit den Einschränkungen, die Docker vs Podman behandelt.

Häufige Fehler

  • Secrets, die in compose.yaml eingecheckt sind. Die Datei landet in Git. Verwenden Sie .env (in gitignore) oder Docker Secrets.
  • Sich auf depends_on verlassen, um auf die Bereitschaft eines Service zu warten. Für sich allein wartet es darauf, dass der Container startet, nicht darauf, dass der Service Verbindungen annimmt. Kombinieren Sie es mit einem healthcheck und condition: service_healthy.
  • container_name setzen. Das verhindert, dass Sie mehr als eine Kopie des Projekts ausführen, und docker compose up --scale funktioniert nicht mehr. Lassen Sie Compose die Container benennen.
  • Einen Bind-Mount über ein Verzeichnis mit installierten Abhängigkeiten legen. Den Projektordner in einen Node- oder Python-Container zu mounten, kann das node_modules oder virtualenv verdecken, das während des Builds erstellt wurde. Mounten Sie Unterordner des Quellcodes, oder legen Sie ein anonymes Volume auf den Abhängigkeitspfad.

Wie Sie eine bestehende App auf Compose umstellen

Nehmen Sie die App, die Sie heute mit einem Skript voller docker run-Zeilen starten, und überführen Sie sie Service für Service in eine compose.yaml. Starten Sie sie, lesen Sie die Logs, beheben Sie, was bricht, fügen Sie den nächsten Service hinzu. Geben Sie allem, wovon andere Services abhängen, einen healthcheck – das ist der Schritt, den man gern überspringt, und genau der, der das “connection refused” beim Kaltstart verhindert.

Sie wissen, dass es geklappt hat, wenn docker compose down -v && docker compose up die ganze Umgebung von Grund auf neu aufbaut und die App danach jedes Mal gleich läuft.

Verwandte Artikel