Compose-Datei: jede Zeile erklärt
Zielgruppe: alle, die eine
docker-compose.ymllesen können, ohne dass jede einzelne Zeile Sinn ergibt. Diese Seite erklärt die Syntax selbst, nicht die Infrastruktur dahinter - für "was startet hier und warum" siehe docker-compose-guide.md und reverse-proxy.md. Beispiele stammen ausschließlich aus den beiden echten Dateien dieses Setups:infra/docker-compose.yml(dieses Projekt) undshared/enterprise-infrastructure/docker-compose.yml(projektübergreifend, sieheshared/enterprise-infrastructure/README.md).Dieses Projekt nutzt Podman. Das Compose-Dateiformat ist mit Docker identisch -
podman compose(bzw. das separatepodman-compose) liest dieselbedocker-compose.yml; der Dateiname bleibt bewusst erhalten.
Was ist eine docker-compose.yml überhaupt?
Eine einzige Textdatei, die beschreibt: "diese Container sollen laufen, mit diesen
Einstellungen, so verbunden." podman compose up -d liest die Datei und setzt genau das um -
Container erstellen, Netzwerke anlegen, alles verbinden. Ohne diese Datei müsstest du dieselbe
Konfiguration als lange podman run ...-Befehlszeile mit zig Flags von Hand eingeben, für JEDEN
Container einzeln, jedes Mal neu.
Die Datei ist YAML (sprich "Jamel") - ein Format, das Struktur über EINRÜCKUNG ausdrückt (wie Python), nicht über Klammern wie JSON. Zwei Regeln reichen für 90 % des Verständnisses:
- Einrückung mit Leerzeichen (nicht Tabs!) zeigt Verschachtelung - eingerückte Zeilen "gehören zu" der weniger eingerückten Zeile darüber.
schlüssel: wertist eine Zuweisung,- elementist ein Listeneintrag.
services: # <- Schlüssel "services" auf oberster Ebene
postgres: # <- eingerückt = gehört zu "services", ist selbst ein Schlüssel ("postgres")
image: postgres:16-alpine # <- noch weiter eingerückt = gehört zu "postgres"
ports: # <- Schlüssel "ports", Wert ist eine LISTE (siehe naechste Zeile)
- "5432:5432" # <- "-" davor = ein Listenelement
Die drei obersten Ebenen
Jede docker-compose.yml in diesem Setup hat dieselbe Grundstruktur - drei Blöcke auf oberster
Ebene (keine Einrückung):
networks: # WELCHE Netzwerke sollen existieren, wie heißen sie
...
volumes: # WELCHE dauerhaften Datenspeicher soll es geben (siehe docker-compose-guide.md,
... # "Was passiert mit meinen Daten?" fuer den vollen Kontext zu Volumes)
services: # DIE EIGENTLICHEN CONTAINER - der groesste, wichtigste Block
...
networks: - welche Netzwerke soll es geben
# infra/docker-compose.yml, Zeilen 22-32
networks:
library-net:
driver: bridge
proxy:
external: true
name: proxy
library-net:- ein NEUES Netzwerk, das DIESE Datei selbst erzeugt (driver: bridgeist der Standard-Netzwerktyp für "Container auf demselben Host, die sich gegenseitig sehen sollen" - praktisch immer die richtige Wahl für lokale Entwicklung).proxy:- kein neues Netzwerk, sondern eine Referenz auf ein BEREITS existierendes, das eine ANDEREdocker-compose.ymlerzeugt hat (shared/enterprise-infrastructure/docker-compose.yml,driver: bridgedort OHNEexternal: true- dort wird es tatsächlich erzeugt).name: proxysagt: "der echte Name in Podman heißt exakt 'proxy'" (ohne das würde Compose einen projektspezifischen Präfix anhängen und das falsche/ein neues Netzwerk suchen). Siehe reverse-proxy.md für den vollen Grund, warum zwei Dateien sich ein Netzwerk teilen.
volumes: - welche dauerhaften Datenspeicher
# infra/docker-compose.yml, Zeilen 34-38
volumes:
pgdata:
kafka-data:
rabbitmq-data:
grafana-data:
Jede Zeile ist ein Name, sonst nichts (kein Doppelpunkt-Wert dahinter nötig) - Compose legt
ein Volume mit diesem Namen an (ein von Podman verwalteter Ordner auf der Platte,
unabhängig vom Container-Lebenszyklus). Erst weiter unten, bei den einzelnen Services, wird
festgelegt, WELCHER Container dieses Volume WOHIN einhängt (siehe volumes: auf Service-Ebene
weiter unten - gleicher Schlüsselname, andere Bedeutung, siehe dort).
Jeder Service - Schlüssel für Schlüssel
Ab hier folgt jeder mögliche Schlüssel innerhalb eines Service-Blocks, in der Reihenfolge, wie sie am häufigsten vorkommen. Jeder Abschnitt zeigt eine ECHTE Fundstelle.
image: - ein fertiges Image aus einer Container-Registry (Docker Hub, Quay.io) verwenden
# infra/docker-compose.yml, Zeile 47
image: postgres:16-alpine
<name>:<tag> - postgres ist der Image-Name (bei offiziellen Images ohne Namespace-Präfix),
16-alpine der Tag (meist eine Version). Podman lädt dieses fertige, bereits gebaute Image
herunter, falls es nicht schon lokal vorhanden ist - kein eigener Bauschritt, im Gegensatz
zu build: (nächster Abschnitt).
build: - selbst aus einem Dockerfile bauen
# infra/docker-compose.yml, Zeilen 251-257 (catalog-service)
build:
context: ../library-platform
dockerfile: Dockerfile
args:
MODULE: catalog-service
PORT: 8081
Alternative zu image: - statt ein fertiges Image zu laden, wird eines aus einem Dockerfile
GEBAUT. context: ist das Verzeichnis, das dem Bauprozess als "Arbeitsbereich" zur Verfügung
steht (alle Dateien darin sind für das Dockerfile sichtbar, sonst nichts) - hier bewusst der
komplette library-platform-Reactor-Root, nicht nur catalog-service/, weil das
Maven-Multi-Modul-Projekt Zugriff auf die anderen Module braucht (common-domain usw.), siehe
Kommentar im Dockerfile selbst. dockerfile: benennt die konkrete Datei innerhalb von
context:. args: reicht Build-Argumente durch, die im Dockerfile über ARG MODULE/
ARG PORT empfangen werden - hier wird EIN generisches Dockerfile für alle neun Services
wiederverwendet, nur MODULE/PORT unterscheiden sich pro Service.
Ein image:-Eintrag KANN trotzdem daneben stehen (nicht in diesem Projekt genutzt) - er würde
dann nur den NAMEN bestimmen, unter dem das selbst gebaute Image lokal gespeichert wird.
container_name: - der Name, den podman ps anzeigt
# infra/docker-compose.yml, Zeile 48
container_name: library-postgres
Ohne diese Zeile würde Compose selbst einen Namen vergeben (nach dem Muster
<projektordner>-<servicename>-1) - hier bewusst überall explizit gesetzt, mit einem
Projekt-Präfix (library-), damit podman ps sofort erkennen lässt, welcher Container zu
welchem Projekt gehört (wichtig, sobald mehrere Projekte gleichzeitig laufen, siehe
reverse-proxy.md).
environment: - Umgebungsvariablen in den Container geben
# infra/docker-compose.yml, Zeilen 259-266 (catalog-service, Auszug)
environment:
SPRING_CONFIG_IMPORT: optional:configserver:http://config-server:8888
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog_db
SPRING_DATA_REDIS_HOST: redis
SCHLÜSSEL: Wert je Zeile - im Container als Umgebungsvariable sichtbar (echo $SPRING_DATASOURCE_URL
würde diesen Wert zeigen). Bei Spring-Boot-Anwendungen übersetzt Spring selbst
SPRING_DATASOURCE_URL automatisch in die Property spring.datasource.url (Punkte werden zu
Unterstrichen, alles groß - "Relaxed Binding", eine Spring-Boot-eigene Konvention, nichts
Compose-Spezifisches). Wichtig: die WERTE hier sind Hostnamen, die INNERHALB des Container-Netzwerks
gelten (postgres, redis, config-server - die jeweiligen container_name- bzw.
Service-Namen), nicht localhost - ein Container kann sich nicht selbst über localhost
erreichen, wenn er einen ANDEREN Container meint.
ports: - Container-Ports auf dem Host sichtbar machen
# infra/docker-compose.yml, Zeile 53-54
ports:
- "5432:5432"
Format "HOST-PORT:CONTAINER-PORT". Der Container lauscht intern auf Port 5432 (rechte Zahl) -
ohne diese Zeile wäre das NUR für andere Container im selben Container-Netzwerk erreichbar. Die
Zeile öffnet zusätzlich einen Port auf deinem Windows-Host (linke Zahl) - localhost:5432
funktioniert dann auch von außerhalb der Container (z. B. von einem Java-Programm, das direkt in
IntelliJ läuft, siehe docker-compose-guide.md). Die beiden Zahlen
müssen NICHT gleich sein - siehe
# infra/docker-compose.yml, RabbitMQ
- "5673:5672" # Host-Port 5673, Container lauscht weiter auf dem Standard-Port 5672
(Grund: ein Portkonflikt mit einem anderen lokalen Projekt, siehe Kommentar an der Fundstelle).
volumes: (auf Service-Ebene) - was wird wohin eingehängt
Drei verschiedene Varianten, alle mit demselben Schlüsselwort:
# infra/docker-compose.yml, Zeilen 55-57 (postgres)
volumes:
- pgdata:/var/lib/postgresql/data
- ./postgres/init-multiple-dbs.sh:/docker-entrypoint-initdb.d/init-multiple-dbs.sh:ro
pgdata:/var/lib/postgresql/data- ein benanntes Volume (der Namepgdatakommt aus demvolumes:-Block ganz oben in der Datei) wird in den Container gehängt. Podman verwaltet, WO das auf der Festplatte wirklich liegt - du musst es nicht wissen, nur den Namen. Daten bleiben über Neustarts/Neuerstellung des Containers hinweg erhalten (siehe docker-compose-guide.md)../postgres/init-multiple-dbs.sh:/docker-entrypoint-initdb.d/init-multiple-dbs.sh:ro- ein Bind-Mount: eine KONKRETE Datei/Ordner von der Festplatte (links,./= relativ zum Ordner dieserdocker-compose.yml) wird an eine bestimmte Stelle IM Container gehängt (rechts). Anders als ein benanntes Volume zeigt ein Bind-Mount auf einen Ort, den DU kennst und direkt bearbeiten kannst (hier: ein Skript, das Postgres beim ersten Start automatisch ausführt).:roam Ende = "read-only", der Container darf diese Datei nur lesen, nicht verändern - sinnvoll für Konfigurationsdateien, die von außen vorgegeben werden.
networks: (auf Service-Ebene) - welchem/welchen Netzwerk(en) beitreten
# infra/docker-compose.yml, Keycloak (nach Phase 12)
networks: [library-net, proxy]
Eckige Klammern mit Kommas sind YAMLs Kurzschreibweise für eine Liste - identisch zu
networks:
- library-net
- proxy
nur kompakter. Ein Container kann an MEHREREN Netzwerken gleichzeitig hängen (wie hier) - er
bekommt dann in JEDEM davon eine eigene IP-Adresse und kann Container in JEDEM davon erreichen.
Genau das nutzt reverse-proxy.md: library-net für den internen
Projekt-Datenverkehr (Postgres, Kafka, ...), zusätzlich proxy NUR damit Traefik (das selbst
auch im proxy-Netz hängt, aber nicht in library-net) diesen Container erreichen kann.
depends_on: - Startreihenfolge
Zwei Varianten, unterschiedlich streng:
# infra/docker-compose.yml, kafka-ui
depends_on: [kafka]
Einfache Form: "warte, bis der Kafka-Container gestartet ist" (nicht: bis er wirklich BEREIT/funktionsfähig ist - ein Container kann "gestartet" sein und trotzdem noch Sekunden brauchen, bis der eigentliche Dienst darin Anfragen beantwortet).
# infra/docker-compose.yml, catalog-service
depends_on:
postgres: { condition: service_healthy }
Strengere Form: wartet, bis der healthcheck: von postgres (siehe nächster Abschnitt)
tatsächlich ERFOLGREICH war, nicht nur bis der Container läuft. { condition: ... } ist YAMLs
Kurzschreibweise für eine eingerückte Zuordnung in einer Zeile, identisch zu
depends_on:
postgres:
condition: service_healthy
healthcheck: - wie prüft die Container-Laufzeit, ob ein Dienst wirklich bereit ist
# infra/docker-compose.yml, postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U library"]
interval: 5s
timeout: 5s
retries: 10
test:- der Befehl, der IM Container ausgeführt wird, um "gesund" zu prüfen.CMD-SHELLbedeutet "durch eine Shell interpretieren" (erlaubt z. B. Pipes/Variablen);CMD(ohne-SHELL, siehe RabbitMQ-Beispiel weiter unten) führt den Befehl direkt aus, etwas schneller, aber keine Shell-Features.interval:- wie oft geprüft wird.timeout:- wie lange EIN Prüfversuch maximal dauern darf, bevor er als fehlgeschlagen zählt. Wichtiger Stolperstein, real erlebt: auf einer stark ausgelasteten Maschine (viele gleichzeitig laufende Container) kann ein an sich funktionierender Dienst laenger brauchen als ein knapp bemessenestimeout- der Dienst gilt dann faelschlich als "unhealthy", obwohl er arbeitet (siehe der ausfuehrlich dokumentierte Zipkin-/RabbitMQ-Fall in reverse-proxy.md bzw.shared/enterprise-infrastructure/README.md).retries:- wie viele FEHLGESCHLAGENE Versuche hintereinander nötig sind, bevor der Container offiziell als "unhealthy" gilt.start_period:(nicht bei postgres, aber bei RabbitMQ in Enterprise Infrastructure genutzt) - eine Anlaufzeit, in der fehlgeschlagene Versuche NICHT gegenretriesgezählt werden - für Dienste, die grundsätzlich lange zum Hochfahren brauchen.
podman ps zeigt das Ergebnis: (healthy), (unhealthy), oder (health: starting) neben dem
Container-Status.
command: und entrypoint: - was beim Containerstart wirklich ausgeführt wird
Jedes Image hat einen eingebauten Standard-Startbefehl (im Dockerfile festgelegt, das das
Image gebaut hat) - command: und entrypoint: überschreiben das:
# infra/docker-compose.yml, keycloak
command: ["start-dev", "--import-realm"]
Einfachster Fall: nur command: gesetzt - überschreibt die STANDARD-Argumente, der
ENTRYPOINT des Images bleibt unverändert und bekommt diese Argumente übergeben.
# shared/enterprise-infrastructure/docker-compose.yml, rabbitmq (Stolperstein-Fix, siehe README.md)
entrypoint: ["sh", "-c"]
command:
- |
echo -n "$$RABBITMQ_ERLANG_COOKIE" > /var/lib/rabbitmq/.erlang.cookie
chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
chmod 400 /var/lib/rabbitmq/.erlang.cookie
exec docker-entrypoint.sh rabbitmq-server
Selteneren, aber hier wichtigen Fall: BEIDE überschrieben. entrypoint: ["sh", "-c"] sagt "starte
eine Shell und führe darin AUS, was in command: steht" - command: enthält hier ein
MEHRZEILIGES Shell-Skript (das | am Zeilenanfang ist YAMLs Zeichen für "alles danach ist ein
mehrzeiliger Text-Block, Zeilenumbrüche bleiben erhalten"). Das Skript repariert erst
eigenhändig eine kaputte Datei-Berechtigung (siehe shared/enterprise-infrastructure/README.md, Abschnitt
"Stolperstein: .erlang.cookie"), bevor es am Ende exec docker-entrypoint.sh rabbitmq-server
aufruft - das ist der EIGENTLICHE, normale Einstiegspunkt des Original-Images, den wir sonst
komplett ersetzt hätten. $$ (doppeltes Dollarzeichen) statt $: Compose selbst interpretiert
EIN $ als "hier eine Compose-eigene Variable einsetzen" - $$ escaped das zu einem
buchstäblichen $, der dann von der SHELL (nicht von Compose) als Umgebungsvariable
$RABBITMQ_ERLANG_COOKIE gelesen wird.
profiles: - Dienste, die NICHT automatisch mitstarten
# shared/enterprise-infrastructure/docker-compose.yml, postgres
profiles: ["postgres"]
Ein Service OHNE profiles:-Zeile startet immer mit podman compose up -d mit. Ein Service MIT
profiles: ["postgres"] startet NUR, wenn du dieses Profil explizit anforderst:
podman compose --profile postgres up -d
Genutzt für alles, was nicht JEDER braucht, der Enterprise Infrastructure startet (Postgres, Keycloak,
Prometheus/Grafana, RabbitMQ - alle optional, siehe shared/enterprise-infrastructure/README.md). Mehrere Profile
gleichzeitig: --profile postgres --profile keycloak (nötig, wenn ein Dienst wie Keycloak einen
ANDEREN profilgesteuerten Dienst als Voraussetzung hat, siehe dortiges depends_on:).
labels: - Metadaten, die Traefik automatisch ausliest
# infra/docker-compose.yml, keycloak (nach Phase 12)
labels:
- "traefik.enable=true"
- "traefik.http.routers.library-keycloak.rule=Host(`keycloak.library.localhost`)"
- "traefik.http.services.library-keycloak.loadbalancer.server.port=8080"
- "traefik.docker.network=proxy"
Für Podman selbst sind Labels nur beliebiger Freitext (Schlüssel-Wert-Paare, die an einem
Container "kleben") - Traefik (siehe reverse-proxy.md) liest Labels mit
dem Präfix traefik. gezielt aus und routet danach automatisch. Vollständige Erklärung dieser
vier Zeilen dort - hier nur die Compose-Syntax-Seite: labels: ist wie ports:/networks:
eine LISTE von Zeichenketten (- "..."), jede im Format schlüssel=wert.
Ein Service komplett gelesen (RabbitMQ, shared/enterprise-infrastructure/docker-compose.yml)
Zum Abschluss ein vollständiger, real existierender Service-Block mit JEDER Zeile kommentiert - bewusst RabbitMQ gewählt, weil er die meisten der oben erklärten Konzepte gleichzeitig zeigt:
rabbitmq: # Service-Name (auch der DNS-Name im Netzwerk)
image: rabbitmq:3.13-management-alpine # fertiges Image aus einer Registry, kein eigener Bau
container_name: shared-rabbitmq # Name in "podman ps"
profiles: ["rabbitmq"] # startet NUR mit "--profile rabbitmq"
entrypoint: ["sh", "-c"] # eigene Shell statt des Standard-Einstiegspunkts
command: # das Shell-Skript, das die Shell ausfuehrt
- |
echo -n "$$RABBITMQ_ERLANG_COOKIE" > /var/lib/rabbitmq/.erlang.cookie
chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
chmod 400 /var/lib/rabbitmq/.erlang.cookie
exec docker-entrypoint.sh rabbitmq-server
environment:
RABBITMQ_ERLANG_COOKIE: "shared-infra-local-only-cookie" # von obigem Skript gelesen
volumes:
- rabbitmq-data:/var/lib/rabbitmq # benanntes Volume (Daten)
- ./rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro # Bind-Mount, nur lesbar
- ./rabbitmq/definitions.json:/etc/rabbitmq/definitions.json:ro
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] # "CMD" = direkt, keine Shell noetig
interval: 15s # alle 15s pruefen
timeout: 30s # bis zu 30s Zeit fuer EINEN Versuch (grosszuegig, siehe oben)
retries: 5 # 5 Fehlversuche hintereinander = "unhealthy"
start_period: 60s # erste 60s zaehlen Fehlversuche nicht mit
networks: [proxy] # nur EIN Netzwerk noetig (kein "library-net"-Aequivalent hier)
labels:
- "traefik.enable=true"
- "traefik.http.routers.rabbitmq.rule=Host(`rabbitmq.localhost`)"
- "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
Kein depends_on:, build: oder ports: in diesem Block - nicht jeder Service braucht jeden
Schlüssel, nur die, die für ihn sinnvoll sind (RabbitMQ hier braucht z. B. keinen Host-Port,
weil er ausschließlich über Traefik erreicht wird, siehe reverse-proxy.md).
Wo geht's weiter?
Mit diesem Vokabular sind infra/docker-compose.yml und
shared/enterprise-infrastructure/docker-compose.yml vollständig lesbar - jeder der neun Fachservice-Blöcke im
Referenzprojekt folgt demselben Muster wie catalog-service oben, nur mit anderen MODULE-/
PORT-Werten und Umgebungsvariablen. Für das WARUM hinter den konkreten Entscheidungen (welche
Ports, welche Netzwerke, welche Healthchecks) siehe
docker-compose-guide.md und reverse-proxy.md.