Compose-Guide (Podman)
Zielgruppe: DevOps / jede:r, der/die lokal die Infrastruktur braucht. Falls die
docker-compose.yml-Syntax selbst (nicht nur WAS hier läuft, sondern WIE man die Datei liest) noch unklar ist: docker-compose-syntax-erklaert.md geht jede Zeile einzeln durch.
Voraussetzung: Podman
Dieses Projekt nutzt Podman (rootless, daemonless) statt Docker Desktop.
- Windows / macOS: Podman braucht eine Linux-VM. Einmalig
podman machine init, danach bei jedem Sitzungsstartpodman machine start. - Linux: kein VM-Schritt nötig; für Testcontainers zusätzlich den User-Socket aktivieren
(
systemctl --user enable --now podman.socket). - Compose-Werkzeug:
podman compose(in Podman 4.1+ eingebaut) ODER das separatepodman-compose(pip install podman-compose). Beispiele hier nutzenpodman compose. - Das externe Netz
proxy(für Traefik, siehe reverse-proxy.md) einmalig anlegen, falls Enterprise Infrastructure noch nicht lief:podman network create proxy.
Die Datei heißt weiterhin docker-compose.yml (und Dockerfile) - Podman liest beide
unverändert, und alle Pfad-Verweise in dieser Doku bleiben gültig.
Starten
cd infra
podman compose up -d # alles im Hintergrund starten
podman compose ps # Status aller Container prüfen
podman compose logs -f kafka # Logs eines einzelnen Containers verfolgen
Stoppen / Zurücksetzen
podman compose down # Container stoppen, Volumes (Daten) bleiben erhalten
podman compose down -v # Container UND Volumes löschen - kompletter Neustart
Was passiert mit meinen Daten? Image, Container und Volume sind drei verschiedene Dinge
Ein haeufiges Missverstaendnis: "die Container-Laufzeit loeschen/neu installieren" klingt nach einem kompletten Reset, ist es aber meistens nicht. Drei Ebenen, die unabhaengig voneinander existieren:
| Ebene | Was es ist | Was mit den Daten passiert |
|---|---|---|
| Image | Die "Bauanleitung" (z. B. postgres:16-alpine oder ein selbst gebautes Service-Image) |
Enthaelt KEINE Laufzeitdaten - ein neues/aktualisiertes Image aendert nichts an bereits laufenden Daten. |
| Container | Eine laufende (oder gestoppte) Instanz eines Images | podman compose down entfernt den Container, aber NICHT die Volumes, die er eingehaengt hatte - beim naechsten podman compose up -d entsteht ein neuer Container, der dieselben Volumes wieder einhaengt. |
| Volume | Der eigentliche, dauerhafte Datenspeicher (pgdata, kafka-data, ...) |
Existiert unabhaengig von Image und Container, bis er EXPLIZIT geloescht wird (podman compose down -v, podman volume rm <name>, oder podman system reset / podman machine reset als Radikal-Loesung). |
Praktische Konsequenz: Die Podman-Machine stoppen (podman machine stop), den Rechner neu
starten, oder Podman komplett deinstallieren und neu installieren, loescht die Volumes in der
Regel NICHT automatisch - sie liegen im Podman-Storage (unter Windows/macOS in der
podman-machine-VM), die eine normale De-/Neuinstallation unberuehrt laesst. Wirklich verloren
gehen die Daten nur, wenn du podman compose down -v ausfuehrst, einzelne Volumes per
podman volume rm <name> loeschst, oder mit podman system reset bzw. podman machine reset
bewusst alles verwirfst.
Ein neues Image ziehen/bauen aendert die Daten NICHT
Wenn du z. B. eine neuere postgres:16-alpine holst (podman compose pull bzw.
podman compose build --pull), betrifft das nur die Anwendungs-/Server-Software selbst - die
Daten im zugehoerigen Volume bleiben unangetastet, weil sie an einem eigenen, vom Image
unabhaengigen Ort liegen. Dasselbe gilt fuer Jenkins (laeuft projektuebergreifend in
shared/enterprise-infrastructure/): ein neu gebautes Jenkins-Image aendert nichts an bereits existierenden
Jenkins-Jobs, Build-Historie oder Credentials im jenkins-data-Volume.
Sonderfall Jenkins-Plugins - Image und Volume wirken hier bewusst ZUSAMMEN:
shared/enterprise-infrastructure/jenkins/Dockerfile (Nachbar-Repo) installiert die Plugins aus
plugins.txt beim Bauen zunaechst nur in einen Referenz-Ordner IM Image
(/usr/share/jenkins/ref/plugins/). Beim ERSTEN Start eines Containers mit diesem Image kopiert
Jenkins' eigenes Start-Skript diese Referenz-Plugins in den echten Plugin-Ordner
(/var/jenkins_home/plugins/) - und der liegt im jenkins-data-Volume. Ab da sind es die
Plugins IM VOLUME, die tatsaechlich geladen werden, nicht mehr die im Image. Bei jedem weiteren
Start vergleicht Jenkins Versionen: eine neuere Plugin-Version aus einem frisch gebauten Image
wird ins Volume nachgezogen, eine gleiche/aeltere wird ignoriert (das Volume "gewinnt"). Wird ein
Plugin aus plugins.txt entfernt, verschwindet es dadurch NICHT automatisch aus einem bereits
bestehenden Volume - es muesste manuell in der Jenkins-UI deinstalliert werden. Nur bei einem
komplett neuen/leeren jenkins-data-Volume bestimmt plugins.txt allein den Zustand.
Port-Tabelle
Seit Phase 12 hat jeder Dienst mit Weboberfläche zusätzlich einen schönen, sprechenden Hostnamen
über einen zentralen Reverse-Proxy (Traefik) - siehe reverse-proxy.md für den
vollen Kontext. Der localhost:<port>-Zugriff bleibt unverändert zusätzlich bestehen.
| Komponente | Host-Port | Schöner Name | Zweck |
|---|---|---|---|
| PostgreSQL | 5432 | - | catalog_db, member_db, lending_db, reservation_db, fine_db, notification_db, keycloak_db |
| pgAdmin | 5050 | pgadmin.library.localhost |
Web-UI für Postgres (Login: admin@library-platform.com / admin) |
| Redis | 6379 | - | Cache (ab Phase 2 genutzt) |
| Kafka | 9092 | - | Event-Bus (Broker-Adresse für externe Clients: localhost:9092) |
| Kafka UI | 8090 | kafka-ui.library.localhost |
Web-UI zum Inspizieren von Topics/Nachrichten |
| RabbitMQ (AMQP) | 5673 | - | Task-/Command-Queues (Host-Port wegen eines lokalen Portkonflikts auf 5673 statt 5672 verschoben, siehe "Bekannte Stolpersteine" unten) |
| RabbitMQ Management | 15673 | rabbitmq.library.localhost |
Web-UI (Login: library / library) |
| Keycloak | 8180 | keycloak.library.localhost |
Identity Provider (Admin-Konsole: Login admin/admin) |
| Prometheus | 9090 | prometheus.library.localhost |
Metriken, seit Phase 7 mit echten Daten (siehe observability.md) |
| Grafana | 3000 | grafana.library.localhost |
Dashboards (Login: admin/admin) |
| Zipkin | 9411 | zipkin.library.localhost |
Distributed Tracing, seit Phase 7 mit echten Traces |
| config-server | 8888 | config.library.localhost |
Zentrale Konfiguration (native Backend) - Check: curl localhost:8888/catalog-service/default |
| discovery-server | 8761 | discovery.library.localhost |
Eureka-Registry - Dashboard: http://localhost:8761 |
| api-gateway | 8080 | api.library.localhost |
Single Entry Point - alle /api/**-Aufrufe hier hinein, nicht direkt an die Fachservices |
| catalog-service | 8081 | - | |
| member-service | 8082 | - | |
| lending-service | 8083 | - | |
| reservation-service | 8084 | - | |
| fine-service | 8085 | - | |
| notification-service | 8086 | - | |
| Angular-Frontend (dev) | 4200 | - | ng serve (siehe library-frontend/README.md) |
Zwei Betriebsarten (seit Phase 7)
1) Alles containerisiert: podman compose up -d baut (Erstlauf: einige Minuten, siehe
library-platform/Dockerfile) und startet ALLE neun Spring-Boot-Services plus die komplette
Infrastruktur. So testest du am naechsten an "echtem" Deployment-Verhalten (Container-Netzwerk,
Umgebungsvariablen statt IDE-Defaults) - siehe die environment:-Bloecke je Fachservice in
docker-compose.yml fuer die dabei ueberschriebenen Werte.
2) Nur Infrastruktur containerisiert (der bisherige Phase-1-6-Workflow): gezielt einzelne
Container starten, z. B. podman compose up -d postgres kafka rabbitmq redis keycloak config-server discovery-server, und EINEN Service weiterhin aus der IDE heraus starten (Run-
Configuration oder mvn spring-boot:run) - praktisch beim Debuggen mit Breakpoints/Hot-Reload,
da die IDE-gestartete Instanz per Default weiterhin gegen localhost zeigt (siehe die jeweilige
application.yml, ohne aktives Profil).
Stolperstein: Keycloak-Issuer vs. Schluesselabruf (JWKS)
Der Browser meldet sich beim Login immer ueber localhost:8180 bei Keycloak an (siehe
library-frontend/auth.config.ts) - Keycloak leitet den iss-Claim jedes ausgestellten Tokens
vom Host-Header DIESER Anfrage ab. Ein containerisierter Backend-Service kann Keycloak aber nicht
ueber localhost:8180 erreichen (das waere der Container selbst), sondern nur ueber den
Container-Namen keycloak:8080. Spring Security wuerde bei issuer-uri: http://keycloak:8080/...
zwar erfolgreich den Signierschluessel abrufen, aber JEDES Token als ungueltig ablehnen (der
iss-Claim im Token sagt localhost:8180, nicht keycloak:8080 - Mismatch). Deshalb werden in
docker-compose.yml bewusst ZWEI unterschiedliche Properties gesetzt:
issuer-uri=http://localhost:8180/... (fuer die reine iss-Claim-Pruefung, muss zum Token
passen) und jwk-set-uri=http://keycloak:8080/.../certs (fuer den tatsaechlichen
Netzwerkaufruf, der aus dem Container heraus funktionieren muss). Ausfuehrlicher erklaert im
Kommentar in api-gateway/application.yml.
Testcontainers mit Podman
Die Testcontainers-*IT-Tests (mvn verify) brauchen einen erreichbaren, Docker-kompatiblen
Container-Socket. Mit Podman:
- Podman-Machine starten (Windows/macOS):
podman machine start. Auf Linux stattdessen den User-Socket aktivieren:systemctl --user enable --now podman.socket. ~/.testcontainers.propertiesanlegen - Vorlage:infra/testcontainers/testcontainers.properties.example. Darin die zur Plattform passendedocker.host-Zeile einkommentieren:- Windows:
docker.host=npipe:////./pipe/podman-machine-default - Linux rootless:
docker.host=unix:///run/user/1000/podman/podman.sock
- Windows:
- Ryuk ist projektweit deaktiviert (
TESTCONTAINERS_RYUK_DISABLED=true, gesetzt immaven-failsafe-plugininlibrary-platform/pom.xml, zusaetzlichryuk.disabled=truein der Properties-Datei fuer IDE-Laeufe). Ryuk laeuft auf rootless Podman - besonders unter Windows - unzuverlaessig; das Aufraeumen der Testcontainer uebernimmt stattdessen Testcontainers' JVM-Shutdown-Hook.
Danach: cd library-platform && mvn -q verify -pl catalog-service -am - die *IT-Tests laufen
durch. Pruefen, dass der Socket erreichbar ist: podman info bzw.
podman info --format '{{.Host.RemoteSocket.Path}}'.
Historie: unter Docker Desktop scheiterten diese Tests auf der Referenzmaschine an einer
docker-java/npipe-Inkompatibilitaet (leerer Stub-400 beim Zugriff ueber die Windows-Named- Pipe) - genau einer der Gruende fuer den Wechsel auf Podman. Die CI (Linux-Runner) war davon nie betroffen und laeuft jetzt ebenfalls gegen den rootless Podman-Socket.
Bekannte Stolpersteine
podman buildscheitert NICHT, aberpodman runmeldetno main manifest attribute: war bis Phase 7 ein echter Bug in diesem Projekt (fehlenderepackage-Goal-Bindung im Parent- POM) - siehe den ausfuehrlichen Stolperstein in den Coding Guidelines. Falls du diesen Fehler nach eigenen POM-Aenderungen siehst: pruefe, obspring-boot-maven- pluginnoch die<executions><goal>repackage</goal></executions>aus dem Parent erbt.- Keycloak startet vor Postgres fertig ist: durch
depends_on: condition: service_healthybereits abgefangen - Keycloak wartet, bispg_isreadyerfolgreich ist. - Kafka braucht einen Moment nach dem ersten Start (KRaft-Cluster-Initialisierung) - bei
Connection refusedvon einem Java-Service beim allerersten Start: 10-20 Sekunden warten oderpodman compose logs kafkaauf "started (kafka.server.KafkaServer)" prüfen. - Portkonflikte mit anderen lokal laufenden Projekten: falls du parallel weitere Lernprojekte
aus diesem Repo mit eigener Infrastruktur laufen hast (z. B. ein anderes Projekt mit eigenem
RabbitMQ- oder Postgres-Container), kann
podman compose upmitaddress already in usefehlschlagen. Prüfen mitpodman ps --format "{{.Names}}\t{{.Ports}}", welcher Container den Port belegt - NICHT fremde Container einfach stoppen, sondern entweder das andere Projekt beenden oder in dieserdocker-compose.ymlden Host-Port anpassen (z. B."15673:15672"statt"15672:15672"). - Windows-Zeilenenden in
init-multiple-dbs.sh: falls das Skript nicht ausgeführt wird ("\r: command not found" im Log), Datei mit LF statt CRLF speichern (Git-Attribut* text=auto eol=lfwäre eine saubere Dauerlösung, hier bewusst nicht vorkonfiguriert, um es als Lernmoment zu belassen).