Files
claude-workflow/skills/docker-compose/SKILL.md
T
martinandClaude Opus 4.7 a93cfef13b 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>
2026-06-16 07:53:58 +02:00

10 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 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).
  • 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)

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
├── .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

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