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.

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:

  1. Podman-Machine starten (Windows/macOS): podman machine start. Auf Linux stattdessen den User-Socket aktivieren: systemctl --user enable --now podman.socket.
  2. ~/.testcontainers.properties anlegen - Vorlage: infra/testcontainers/testcontainers.properties.example. Darin die zur Plattform passende docker.host-Zeile einkommentieren:
    • Windows: docker.host=npipe:////./pipe/podman-machine-default
    • Linux rootless: docker.host=unix:///run/user/1000/podman/podman.sock
  3. Ryuk ist projektweit deaktiviert (TESTCONTAINERS_RYUK_DISABLED=true, gesetzt im maven-failsafe-plugin in library-platform/pom.xml, zusaetzlich ryuk.disabled=true in 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

⌂ Cockpit