From a93cfef13b4383c28ec0ff76da378c4f30e0620d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tr=C3=B6ger?= Date: Tue, 16 Jun 2026 07:53:58 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20/docker-compose=20Skill=20f=C3=BCr=20Co?= =?UTF-8?q?mpose-Stacks=20nach=20Hausregeln?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skill fragt vier Zielhosts ab (automation, controller, webserver, localhost), prüft Port-Belegung gegen bestehende Compose-Files und aktive Listener und generiert eine docker-compose.yaml plus .env nach den Hausregeln (5 MB Logging, TZ Berlin, relative Bind-Mounts, Secrets in .env). Auf webserver werden interne Ports an die VPN-IP 10.7.0.1 gebunden. Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 1 + skills/docker-compose/SKILL.md | 260 +++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 skills/docker-compose/SKILL.md diff --git a/CLAUDE.md b/CLAUDE.md index 62fb688..4a31119 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,6 +104,7 @@ Kurzbefehl für den gesamten Ablauf: `/story` | `/workflow-review` | `skills/workflow-review/SKILL.md` | Opus reviewt alle Skills/Konfig → Umsetzungsplan für Sonnet | | `/implement` | `skills/implement/SKILL.md` | Implementierung direkt aus bestehendem Plan (überspringt Research/Planung) | | `/project-review` | `skills/project-review/SKILL.md` | Domänenunabhängiges Review für Nicht-Software-Dokumente (Bau, Hardware, Analysen) | +| `/docker-compose` | `skills/docker-compose/SKILL.md` | Erstellt/aktualisiert eine docker-compose.yaml nach Hausregeln (4 Zielhosts: automation/controller/webserver/localhost, Port-Belegungs-Check, 5 MB Logging, TZ Berlin, Bind-Mounts, .env-Secrets, VPN-Binding für Webserver) | ## Agenten diff --git a/skills/docker-compose/SKILL.md b/skills/docker-compose/SKILL.md new file mode 100644 index 0000000..fe9a275 --- /dev/null +++ b/skills/docker-compose/SKILL.md @@ -0,0 +1,260 @@ +--- +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 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). +- Bei `:latest` + Watchtower-Enable=`true` → automatische Updates. +- Bei kritischen Stacks (Datenbanken, Auth) kann ein gepinnter Tag sinnvoll sein — explizit beim User nachfragen. + +### 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.