feat: /docker-compose Skill für Compose-Stacks nach Hausregeln
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
| `/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) |
|
| `/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) |
|
| `/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
|
## Agenten
|
||||||
|
|
||||||
|
|||||||
@@ -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/<name>/` 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/<name>/` | internes LAN, kein VPN-Binding |
|
||||||
|
| `controller` | `ssh controller:/docker/<name>/` | internes LAN, kein VPN-Binding |
|
||||||
|
| `webserver` | `ssh waldperle:/docker/<name>/` | ö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 <host> "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 <host> "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: <stack>`** 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: <name>`** — explizit, stabile Namen für Logs/Scripts.
|
||||||
|
- **`hostname: <name>`** — 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 `<name>_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
|
||||||
|
|
||||||
|
```
|
||||||
|
<stack-name>/
|
||||||
|
├── docker-compose.yaml
|
||||||
|
├── .env (lokal, nicht in Git)
|
||||||
|
├── .env.example (Vorlage in Git)
|
||||||
|
├── .gitignore (mind. .env, dazu Daten-Verzeichnisse)
|
||||||
|
└── <persistente Verzeichnisse, z.B. ./data, ./db, ./config>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <pfad>/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 <pfad>/docker-compose.yaml config
|
||||||
|
|
||||||
|
# Start
|
||||||
|
cd <pfad> && docker compose up -d
|
||||||
|
|
||||||
|
# Logs prüfen
|
||||||
|
docker compose logs -f <service>
|
||||||
|
|
||||||
|
# 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/<name>/` 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.
|
||||||
Reference in New Issue
Block a user