Files
claude-workflow/skills/docker-compose/SKILL.md
T
martinandClaude Opus 4.7 2eb4277aaa feat: INSTALL.md pro Stack im /docker-compose Skill
Phase 3 erweitert um einen Pflichtschritt 3e: INSTALL.md wird neben der
docker-compose.yaml im Stack-Verzeichnis erzeugt. Sie enthält
Voraussetzungen, Verzeichnis-Setup, .env-Befüllung, Start, Erst-Konfiguration,
Backup, Update inkl. Breaking-Change-Hinweisen aus Phase 0.5 sowie
Troubleshooting. Bei Updates wird die existierende INSTALL.md ergänzt,
nicht überschrieben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 08:04:29 +02:00

16 KiB
Raw Blame History

description, model
description model
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. 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 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/_/<image>           # Official Images
    https://hub.docker.com/r/<vendor>/<image>  # 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:

# Stack: <name>
# Recherchiert: <YYYY-MM-DD>
# Images:
#   postgres:latest    → 17.x  (Breaking: Major-Upgrade braucht pg_dumpall)
#   <image>: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

# 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

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 80008999, DBs 5400+, Custom 30003999).

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:
    logging:
      driver: "json-file"
      options:
        max-size: "5M"
        max-file: "3"
    
    5 MB pro Datei × 3 Dateien = max. 15 MB pro Container.
  • labels:
    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:
    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)

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:

    # 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
├── INSTALL.md        (Schritt-für-Schritt-Anleitung)
├── .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. Die konkreten Befehle landen in der INSTALL.md (3e).

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.

3e: INSTALL.md erzeugen (Pflicht)

Schritt-für-Schritt-Anleitung im Stack-Verzeichnis. Alle Installationshinweise des Skills landen hier — nicht als Chat-Output, sondern als Datei. Vorlage:

# <Stack-Name> — Installation

Recherchiert: <YYYY-MM-DD>
Zielhost: <automation | controller | webserver | localhost>

## 1. Voraussetzungen

- Docker + Docker Compose v2 installiert
- Externe Netzwerke vorhanden (falls in Compose referenziert):
  ```bash
  docker network ls | grep -E 'reverse-proxy|public|<weitere>'
  # Falls fehlend:
  docker network create reverse-proxy
  ```

## 2. Verzeichnis vorbereiten

```bash
cd /docker/<stack-name>
mkdir -p ./data ./db   # alle persistenten Bind-Mounts aus dem Compose-File
chown -R 1000:1000 ./data ./db
```

## 3. .env befüllen

```bash
cp .env.example .env
chmod 600 .env
$EDITOR .env
```

Pflicht-Variablen:
- `<VAR_1>` — <Bedeutung, Format, Quelle (Passwort-Generator? KeePass?)>
- `<VAR_2>` — ...

## 4. Stack starten

```bash
docker compose config              # Trockenlauf — keine Fehler erwartet
docker compose up -d
docker compose ps                  # alle Services healthy?
docker compose logs -f <service>   # auf Initial-Output prüfen
```

## 5. Erst-Konfiguration

- Web-UI: `http://<host>:<port>` (bei webserver via VPN: `http://10.7.0.1:<port>`)
- Initial-Login: <falls dokumentiert — sonst Hinweis auf Setup-Wizard>
- Reverse-Proxy-Eintrag in NPM anlegen (falls reverse-proxy-Netz genutzt)

## 6. Backup

Persistente Verzeichnisse: `./data`, `./db`, `<weitere>`
Backup-Strategie: <restic? rsync? Hinweis auf zentrale Backup-Pipeline>

## 7. Update

```bash
docker compose pull
docker compose up -d
```

**Breaking Changes / Major-Upgrade-Hinweise:**
<aus Phase 0.5 — z.B. "PostgreSQL Major-Sprung erfordert pg_dumpall vorher">

## 8. Troubleshooting

- Logs: `docker compose logs <service>`
- Shell: `docker compose exec <service> sh`
- Vollständiger Reset (Achtung — Daten weg!): `docker compose down -v && rm -rf ./data ./db`

Befülle die Platzhalter aus den Phasen 0, 0.5, 1.5 und 2:

  • Stack-Name, Zielhost, Recherche-Datum: aus Phase 0/0.5
  • Pflicht-Variablen und ihre Bedeutung: aus der .env.example (3d)
  • Breaking-Change-Hinweise: aus der Container-Recherche (Phase 0.5)
  • Ports + Bind-IP-Beispiele: aus Phase 1.5 + 2 (bei webserver inkl. 10.7.0.1)
  • Persistente Verzeichnisse: aus den Bind-Mounts der Compose-Datei

Bei Update eines bestehenden Stacks: existierende INSTALL.md lesen, gezielt ergänzen — nicht überschreiben.

Phase 4: Validierung

# 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

# 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.