--- description: Erstellt oder aktualisiert eine docker-compose.yaml nach den Best Practices dieses Workflows — aktuelle Compose-Syntax, explizite Konfiguration, Secrets in .env, optionales VPN-Binding für den Webserver. Aktivieren wenn ein neuer Container-Stack aufgesetzt oder ein bestehender überarbeitet werden soll. Nicht verwenden für reine Image-Updates, einzelne docker-Befehle oder Dockerfile-Änderungen. model: claude-opus-4-6 --- # /docker-compose — Compose-Stack nach Hausregeln Zielort: ein Verzeichnis unter `/docker//` auf einem der Hosts oder lokal. Der Skill liefert eine `docker-compose.yaml` plus passende `.env`-Vorlage. ## Phase 0: Aufgabenklärung Stelle dem User folgende Fragen, sofern nicht schon im Request beantwortet: 1. **Name des Stacks** (Verzeichnisname unter `/docker/`) 2. **Welche Services** sind enthalten (Image + Zweck)? 3. **Zielhost** — eine der folgenden vier Optionen, per `AskUserQuestion`: | Ziel | SSH-Pfad | Netzwerk-Kontext | |---|---|---| | `automation` | `ssh automation:/docker//` | internes LAN, kein VPN-Binding | | `controller` | `ssh controller:/docker//` | internes LAN, kein VPN-Binding | | `webserver` | `ssh waldperle:/docker//` | öffentlich, interne Ports an VPN `10.7.0.1` binden | | `localhost` | aktuelles Arbeitsverzeichnis | kein Bind-Prefix | 4. **Reverse-Proxy** — soll der Stack über `nginx-proxy-manager` ans Web? Dann `reverse-proxy`-Netz nötig. 5. **Persistente Daten** — welche Verzeichnisse müssen über Restarts/Recreates überleben (Datenbankdaten, Configs, Uploads, Logs)? 6. **Gewünschte Host-Ports** — welche Ports sollen extern erreichbar sein? (werden in Phase 1.5 gegen Belegung geprüft) Bei `webserver` zusätzlich: > "Sollen die Ports nur über das VPN (`10.7.0.1`) erreichbar sein oder auch öffentlich (z.B. `80`, `443`)?" Standardregel: **alle Ports an `10.7.0.1` binden**, nur explizit öffentliche (HTTP/HTTPS für reverse-proxy) bleiben ohne Bind-IP. ## Phase 0.5: Container-Recherche (Pflicht vor Generierung) Für **jedes** Image im Stack die aktuelle Dokumentation konsultieren, bevor die Compose-Datei geschrieben wird. Ziel: korrekte aktuelle Konfiguration, bekannte Breaking Changes berücksichtigen, deprecated Variablen vermeiden. ### Quellen (in dieser Reihenfolge) 1. **Context7** (bevorzugt für offizielle Images): ``` mcp__plugin_context7_context7__resolve-library-id → Library-ID finden mcp__plugin_context7_context7__query-docs → gezielte Frage stellen ``` 2. **Offizielle Docker-Hub-Seite** via `WebFetch`: ``` https://hub.docker.com/_/ # Official Images https://hub.docker.com/r// # Community Images ``` 3. **GitHub-Releases / CHANGELOG** via `WebFetch` — für Breaking Changes der aktuellen Major-Version. 4. **WebSearch** als Fallback, wenn die obigen Quellen leer bleiben. ### Was muss konkret geprüft werden? Pro Image diese Punkte abklären: - **Aktuelle Major-Version** (`:latest` zeigt auf welche?) - **Breaking Changes** seit der vorherigen Major-Version (z.B. PostgreSQL-Daten-Migration zwischen Major-Versionen, Keycloak `start-dev` vs `start --optimized`, MySQL→MariaDB-Replacements) - **Pflicht-Env-Vars** der aktuellen Version (z.B. Auth-Defaults wurden in vielen Images entfernt — `POSTGRES_PASSWORD` ist Pflicht, kein Standard mehr) - **Deprecated Env-Vars** — alte Namen vermeiden - **Default-Ports** und ob sie sich geändert haben - **Volume-Pfade** des Images — wo erwartet das Image persistente Daten? - **Health-Check-Kommando** — was empfiehlt der Maintainer? - **User/UID-Verhalten** — läuft das Image als Root, gibt es ein `PUID/PGID`-Pattern, oder `user:`-Override möglich? ### Output dieser Phase Bevor weitergegangen wird, dem User eine **Zusammenfassung pro Image** zeigen: ``` postgres:latest → aktuell 17.x Breaking: pg_dump-Format zwischen Major-Versionen inkompatibel — Major-Upgrade erfordert pg_dumpall/restore (siehe Release-Notes) Pflicht: POSTGRES_PASSWORD Empfohlen: POSTGRES_DB, POSTGRES_USER, Healthcheck pg_isready Volume: /var/lib/postgresql/data ``` Bei Major-Sprung gegenüber einem bestehenden Stack: User explizit warnen und Migrationsschritt vorschlagen, bevor `:latest` gepinnt bleibt. ### Im Compose-Header dokumentieren Header-Kommentar in der `docker-compose.yaml`: ```yaml # Stack: # Recherchiert: # Images: # postgres:latest → 17.x (Breaking: Major-Upgrade braucht pg_dumpall) # :latest → x.y (Hinweise/Breaking) ``` So bleibt nachvollziehbar, gegen welchen Wissensstand der Stack designed wurde. ## Phase 1.5: Port-Belegungs-Analyse (Pflicht vor Generierung) Bevor irgendein Port in die `docker-compose.yaml` geschrieben wird, **beide** Prüfungen auf dem Zielhost ausführen: ### Prüfung 1: Alle bestehenden Compose-Files ```bash # Bei automation/controller/webserver via SSH: ssh "grep -rhE '^\s*-\s*\"?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:)?[0-9]+:[0-9]+' /docker/ 2>/dev/null \ | sed -E 's/.*\"?([0-9.]+:)?([0-9]+):.*/\2/' | sort -u" # Bei localhost: lokal ausführen (Pfad ggf. anpassen) grep -rhE '^\s*-\s*\"?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:)?[0-9]+:[0-9]+' . 2>/dev/null \ | sed -E 's/.*\"?([0-9.]+:)?([0-9]+):.*/\2/' | sort -u ``` ### Prüfung 2: Aktive Listener auf dem Host ```bash ssh "ss -tlnp 2>/dev/null | awk 'NR>1 {split(\$4, a, \":\"); print a[length(a)]}' | sort -u" # Bei localhost: ss -tlnp 2>/dev/null | awk 'NR>1 {split($4, a, ":"); print a[length(a)]}' | sort -u ``` ### Konfliktauflösung Vergleiche die in Phase 0 gewünschten Host-Ports mit beiden Listen: - **Frei in beiden:** Port übernehmen. - **Belegt in Prüfung 1 (Compose):** zeigen, welcher Stack/Service belegt — User fragen, ob Konflikt akzeptiert ist (selten korrekt) oder ein anderer Port gewählt werden soll. - **Belegt in Prüfung 2 (Listener):** zeigen welcher Prozess — alternativen Port vorschlagen. - **Vorschlag bei Konflikt:** nächste freie Port-Nummer aus passendem Bereich (Web-UIs `8000–8999`, DBs `5400+`, Custom `3000–3999`). Erst wenn alle gewünschten Ports konfliktfrei sind, weiter zu Phase 3. **Hinweis für `webserver`:** das VPN-IP-Prefix `10.7.0.1` ändert nichts an der Belegungsprüfung — ein bereits gebundener Port ist gebunden, egal an welche IP. ## Phase 1: Pflichtregeln (nicht verhandelbar) Diese Regeln gelten für jeden generierten Stack: ### Compose-Syntax - **Kein `version:`-Key.** Compose v2 ignoriert ihn — nicht aufnehmen. - **`name: `** als oberstes Feld (Project-Name explizit setzen). - **YAML-Mapping-Stil konsistent** pro Datei (entweder Listen-Stil `- KEY=VAL` oder Map-Stil `KEY: VAL` — nicht mischen innerhalb eines Services). ### Pro Service - **`container_name: `** — explizit, stabile Namen für Logs/Scripts. - **`hostname: `** — für interne Service-Adressierung in geteilten Netzwerken. - **`restart: unless-stopped`** als Default. `always` nur wenn der Container nach einem manuellen `docker stop` von alleine wieder hochkommen soll. - **`environment:`** - `TZ=Europe/Berlin` ist Pflicht. - Alle relevanten Env-Variablen explizit auflisten (keine impliziten Defaults aus dem Image — der Image-Default kann sich ändern). - Sensible Werte (Passwörter, Tokens, Secrets, API-Keys) ausschließlich als `${VARIABLE}` referenzieren — Werte in `.env`. - Keine Inline-Defaults vom Typ `${VAR:-default}` für Secrets. Für nicht-sensible Werte erlaubt, aber sparsam. - **`logging:`** ```yaml logging: driver: "json-file" options: max-size: "5M" max-file: "3" ``` 5 MB pro Datei × 3 Dateien = max. 15 MB pro Container. - **`labels:`** ```yaml labels: - "com.centurylinklabs.watchtower.enable=false" ``` Default `false` (entspricht globaler CLAUDE.md). Auf `true` setzen, wenn der Container über `:latest` läuft und automatische Updates erwünscht sind. - **`user: "1000:1000"`** als Default. Ausnahmen nur, wenn das Image Root erfordert (z.B. NPM Port 80/443) — dann mit Kommentar warum. - **Volumes als relative Bind-Mounts** (`./data:/data`). Niemals absolute Pfade ausserhalb von `./`, niemals named volumes für Datenpersistenz (Backup-Strategie hängt am Bind-Mount). - **Read-only mounten** wo möglich (`./config:/etc/app/config:ro`). ### Ports - **Immer als gequoteter String:** `"8080:80"`. Unquoted YAML kann `5060:5060` als Sexagesimalzahl interpretieren. - **Pro Zielhost:** - `automation`, `controller`, `localhost`: kein IP-Prefix (LAN-intern erreichbar). - `webserver` (waldperle): alle internen Ports an `10.7.0.1` binden — `"10.7.0.1:8080:80"`. Nur explizit öffentliche Ports (NPM 80/443) ohne IP-Prefix. - **Belegung muss in Phase 1.5 geprüft sein** — kein Port ohne vorherige Konfliktprüfung. ### Netzwerke - **Externe Standard-Netze:** - `reverse-proxy` — für alle Container, die über NPM ans Web sollen - `public` — für Container mit direktem Public-Exposure - `openhab` — Smart-Home-Stack (nur `automation`) - Externe Netze in der `networks:`-Sektion immer mit `external: true` markieren. - Stack-interne Netze ohne `external` deklarieren (oder weglassen — Compose erzeugt dann `_default`). ### Multi-Service-Stacks - **Healthchecks für DB/Cache:** ```yaml healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 ``` - **`depends_on` mit `condition: service_healthy`** wo Healthchecks existieren — verhindert Race-Conditions beim Start. ### Image-Versionen - **`:latest`** ist der Default (User-Wunsch: immer neueste Version) — aber nur, wenn die in Phase 0.5 dokumentierte Major-Version mit der Stack-Konfiguration kompatibel ist. - Bei `:latest` + Watchtower-Enable=`true` → automatische Updates. Bei Datenbanken nur, wenn das Image rolling-kompatibel ist (oft nicht — siehe PostgreSQL-Hinweis aus Phase 0.5). - Bei kritischen Stacks (Datenbanken, Auth) Major-Version pinnen (z.B. `postgres:17`) und Major-Upgrades bewusst durchführen. ### Security-Hardening (für exposed Services empfohlen) ```yaml cap_drop: - ALL security_opt: - no-new-privileges=true ``` Aufnehmen für: Web-UIs, REST-APIs, alles was Ports öffnet. Nicht für DBs/Caches im internen Netz (würde dort oft brechen). ### `.env`-Datei - Liegt im selben Verzeichnis wie die `docker-compose.yaml`. - Wird per `.gitignore` ausgeschlossen. - Eine `.env.example` ohne Werte mitliefern — als Vorlage. - Format: ``` # Pflicht DB_USER= DB_PASSWORD= # ... ``` ## Phase 2: VPN-Binding für Webserver-Stacks Wenn der User in Phase 0 als Zielhost `webserver` (waldperle) gewählt hat: 1. **Bind-Adresse abfragen** falls nicht bekannt: > "Welche VPN-Interface-IP soll gebunden werden? (Standard `waldperle`: `10.7.0.1`)" 2. **Pro Service-Port entscheiden:** - Public (im Internet erreichbar, typisch Reverse-Proxy 80/443): ohne IP-Prefix. - Alle anderen Ports (Web-UIs, DBs, Admin-Interfaces, Metrics): an die VPN-IP binden. 3. **Begründung im Kopf des Compose-Files als Kommentar dokumentieren:** ```yaml # Webserver-Stack — interne Ports nur via VPN (10.7.0.1) erreichbar. # Nur 80/443 (Reverse-Proxy) sind öffentlich exponiert. ``` ## Phase 3: Generierung ### 3a: Verzeichnis-Layout ``` / ├── docker-compose.yaml ├── .env (lokal, nicht in Git) ├── .env.example (Vorlage in Git) ├── .gitignore (mind. .env, dazu Daten-Verzeichnisse) └── ``` ### 3b: Datei erzeugen Generiere die `docker-compose.yaml` nach den Regeln aus Phase 1 + 2. Vor dem Schreiben prüfen, ob die Datei schon existiert — bei Update nur die nötigen Felder anfassen, vorhandene Kommentare/Strukturen erhalten. ### 3c: Persistente Verzeichnisse anlegen Nur als Hinweis an den User — nicht selbst per `mkdir` ausführen. Beispiel: > "Vor dem ersten Start: `mkdir -p ./data ./db && chown -R 1000:1000 ./data ./db`" ### 3d: `.env.example` erzeugen Liste alle in der Compose-Datei referenzierten `${VARIABLE}` mit leerem Wert. Sortierung: erst Pflicht, dann optional. Jede Variable mit Einzeiler-Kommentar, wozu sie dient. ## Phase 4: Validierung ```bash # Auf dem Zielhost (automation/controller/webserver via SSH, oder lokal): docker compose -f /docker-compose.yaml config ``` Erfolgreich heisst: keine Warnings, kein "version is obsolete", alle `${VAR}` auflösbar (mit gefüllter `.env`). Bei Fehlern: korrigieren, erneut validieren. ## Phase 5: Test-Empfehlung ```bash # Trockenlauf docker compose -f /docker-compose.yaml config # Start cd && docker compose up -d # Logs prüfen docker compose logs -f # Status docker compose ps ``` Warte auf User-Rückmeldung, ob der Stack erfolgreich gestartet ist. ## Phase 6: Abschluss via /ship Wenn die Compose-Datei in einem Git-Repo liegt: `/ship` aufrufen. Liegt sie nur lokal auf dem Host (z.B. direkt unter `/docker//` auf `automation`/`waldperle`, ohne Git): User erinnern, die Konfiguration manuell ins Backup aufzunehmen. ## Pitfalls — explizite Verbote - Kein `version:`-Key (obsolet). - Keine impliziten Image-Defaults für relevante Konfiguration. - Keine ungequoteten Port-Mappings. - Keine absoluten Pfade in Bind-Mounts. - Keine Secrets im Klartext in der Compose-Datei. - Keine named Volumes für persistente Daten. - Auf `webserver` (waldperle): kein ungebundener interner Port — sonst public. - Keine Port-Angabe ohne vorherige Belegungs-Analyse aus Phase 1.5.