# Docker-Compose-Standard (verbindlich) > Gilt für **jeden** Docker-Stack, der hier entsteht. Nicht zur Diskussion stellen – vollständig „durchdeklinieren“. > Abweichungen nur, wenn das Image es technisch erzwingt; dann im Stack-README begründen. > Vorlagen: [`templates/docker/`](templates/docker/). Umgebungswerte (`<…>`) stehen in `ai/context/environment.md`. ## 1. Pflichtbestandteile je Service | # | Vorgabe | Warum | |---|---|---| | 1 | `restart: unless-stopped` | stabiler Betrieb, manuelles Stoppen bleibt respektiert | | 2 | `logging: *default-logging` (GELF → Graylog) | zentrales Logging | | 3 | `container_name: ${COMPOSE_PROJECT_NAME}` (mehrere Services: `${COMPOSE_PROJECT_NAME}_`) | eindeutige Namen, GELF-Tag passt | | 4 | `env_file: .env` statt aufgeblähtem `environment:` | Stackfile bleibt transportabel | | 5 | Persistenz nur per **Bind-Mount** aus `.env` (`${DIR_*}`), keine Named Volumes | Stack-Verzeichnis = komplettes Backup, umziehbar | | 6 | `healthcheck` | „guter Ton“; Test passend zum Image (s. 4.) | | 7 | Traefik-Labels (s. 3.) für jeden Service mit Web-Oberfläche/API | Exposition über Reverse Proxy | | 8 | Image-Tag aus `.env` (`${_TAG}`), **nie** `latest` | reproduzierbar, Updates bewusst | | 9 | **Alle** host-/umgebungsspezifischen Werte in `.env` (Hosts, IPs, Ports, Pfade, Domains, `extra_hosts`) | Stackfile enthält keine Umgebungsdaten | | 10 | **Namen ohne Bindestrich – Unterstrich statt `-`:** `COMPOSE_PROJECT_NAME`, Stack-Verzeichnis, Service-, Container- und Router-Namen (z. B. `hashi_vault`, nicht `hashi-vault`) | GELF-Tag = Projektname; `_` läuft in der Elasticsearch-Suche sauber (`-` wird als Worttrenner zerlegt) | | 11 | **Header** mit Verantwortlichen im `docker-compose.yml` (`rules/headers.md`) | Zuständigkeit sofort sichtbar | ## 2. Verzeichnislayout je Stack ``` // # = COMPOSE_PROJECT_NAME ├── docker-compose.yml ├── .env # echte Werte inkl. Secrets – nie in Git/extern ├── .env.example # gleiche Schlüssel, Platzhalter statt Secrets ├── README.md # Stack-Doku (s. 5.) └── data/ config/ … # Bind-Mount-Ziele (über DIR_* referenziert) ``` ## 3. Vorlagen ### 3.1 `docker-compose.yml` ```yaml x-logging: &default-logging driver: gelf options: gelf-address: "udp://${GELF_HOST}:${GELF_PORT}" tag: "${COMPOSE_PROJECT_NAME}" labels: "com.docker.compose.project,com.docker.compose.service" services: app: image: /:${APP_TAG} container_name: ${COMPOSE_PROJECT_NAME} restart: unless-stopped env_file: .env # ports: nur wenn Zugriff ohne Traefik nötig ist # - "${APP_PORT}:" extra_hosts: # nur wenn benötigt; ein Eintrag je Variable - "${EXTRA_HOST_1}" volumes: - ${DIR_DATA}:/ - ${DIR_CONFIG}:/:ro networks: - proxy labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true (s. 3.2) - "traefik.enable=${TRAEFIK_ENABLE:-false}" - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}" # bei Bedarf manuell ergänzen: entrypoints, middlewares … logging: *default-logging healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:/"] interval: 30s timeout: 5s retries: 3 start_period: 30s networks: proxy: name: ${TRAEFIK_NETWORK:-traefik-net} external: true ``` ### 3.2 Traefik-Labels: Dummy zuerst, Freischaltung per Schalter - Labels stehen **immer** direkt im Stackfile und werden je Stack manuell angepasst (kein `extends`, keine Auslagerung). - `traefik.enable=${TRAEFIK_ENABLE:-false}` → solange in `.env` nicht `TRAEFIK_ENABLE=true` steht, ignoriert Traefik den Container: **kein Router, keine Let's-Encrypt-Anfrage.** - Ablauf: Stack ohne Traefik hochfahren → Healthcheck `healthy`, Funktion geprüft, FQDN/DNS stimmt → erst dann `TRAEFIK_ENABLE=true` + `docker compose up -d`. - Dummy-FQDN `.invalid` (reservierte TLD) verhindert, dass ein vergessener Wert je eine echte Domain anfragt. - Mehrere Web-Services: Router-/Service-Namen eindeutig halten (`${COMPOSE_PROJECT_NAME}_`). ### 3.3 `.env` / `.env.example` ```dotenv # --- Stack --- COMPOSE_PROJECT_NAME= # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag) APP_TAG= # --- Logging (Graylog) --- GELF_HOST= GELF_PORT=12201 # --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) --- TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!) APP_FQDN= APP_INTERNAL_PORT= #TRAEFIK_NETWORK=traefik-net #TRAEFIK_CERTRESOLVER=le # --- Hosts --- EXTRA_HOST_1=: # --- Pfade (Bind-Mounts, relativ zum Stack) --- DIR_DATA=./data DIR_CONFIG=./config # --- Image-spezifische Variablen (laut Orig-Doku, s. README) --- ``` ### 3.4 Geprüfte, aber verworfene Wege (Compose v5.0.2, 06.10.2026) - **`label_file:`** – keine Variablen im Schlüssel möglich (Router-Name!) und Werte werden nicht interpoliert. Unbrauchbar für Traefik. - **`extends` aus gemeinsamer `traefik.yml`** – technisch ok, vom Bediener abgelehnt: Labels gehören sichtbar und manuell anpassbar ins Stackfile. - **Separate `.traefik.env` per `COMPOSE_ENV_FILES` in `.env`** – wirkt nicht; nur als Shell-Variable oder per `--env-file` bei jedem Aufruf → fehleranfällig. ## 4. Hinweise zur Umsetzung - **`extra_hosts` in `.env`:** ja, möglich – Compose interpoliert Listenwerte. Ein Eintrag je Variable (`EXTRA_HOST_1=host01.example.org:192.0.2.10`). Schlüssel in Map-Syntax werden *nicht* interpoliert → Listen-Syntax verwenden. Leere Variable ergibt einen ungültigen Eintrag → Zeile nur aufnehmen, wenn genutzt. - **Healthcheck:** vorher prüfen, ob `wget`/`curl` im Image existieren (`docker run --rm --entrypoint sh -c 'command -v wget curl'`). Endpunkt wählen, der im Normalbetrieb nicht fälschlich failt (z. B. Vault: `/ui/` statt `/v1/sys/health`, weil versiegelt = 503). - **`env_file: .env`** reicht auch Stack-Variablen (GELF_*, DIR_*, TRAEFIK_*) in den Container durch – ist so gewollt und in der Regel harmlos. - Kommentare im Stackfile nur für Nicht-Offensichtliches (wie im Vault-Healthcheck). ## 5. Stack-README (Pflicht) Jeder Stack bekommt ein `README.md` mit: 1. **Zweck** und Zugriff (FQDN, ggf. Port). 2. **Orig-Doku:** Links zu Image-Seite (Docker Hub / GHCR) und Hersteller-Doku, Abrufdatum. 3. **ENV-Variablen laut Orig-Doku:** Tabelle `Variable | Bedeutung | Default | gesetzt in .env?` – vollständig aus der Herstellerdoku übernommen, nicht geraten. 4. **Volumes/Pfade** laut Orig-Doku (welcher Container-Pfad persistiert was). 5. **Ports** intern/extern, Healthcheck-Endpunkt und Begründung. 6. **Besonderheiten** (Capabilities wie `IPC_LOCK`, Init-/Unseal-Schritte, Update-Hinweise, Abweichungen vom Standard). Vor dem Schreiben eines Stacks die Orig-Doku per Web-Recherche holen (erlaubt laut `rules/web_search.md`). ## 6. Abnahme-Checkliste - [ ] Punkte 1–11 aus Abschnitt 1 erfüllt - [ ] Kein `-` und kein `__` in Projekt-, Service-, Container- und Router-Namen - [ ] sultan-Host: Port veröffentlicht, kein externes Traefik-Netz, `entrypoints`-Label (7.1) - [ ] Keine Hosts/IPs/Domains/Pfade/Secrets im Stackfile - [ ] `.env.example` vollständig, ohne Secrets - [ ] README mit Orig-Doku und ENV-Tabelle - [ ] `docker compose config` läuft fehlerfrei - [ ] Healthcheck wird nach Start `healthy` – **erst danach** `TRAEFIK_ENABLE=true` ## 7. Ergänzungen (25.10.2026) ### 7.1 sultan-Variante (Traefik auf anderer VM) Gilt für Hosts, deren Traefik woanders läuft und per [sultan](../context/knowledge/services/sultan.md) angebunden ist. - **Kein** externes Netz `proxy`/`traefik-net`, **kein** Label `traefik.docker.network`. - Web-Service **veröffentlicht** seinen Port: `ports: ["${APP_PORT}:"]` (Bindung `0.0.0.0`; `127.0.0.1` verwirft der sultan-Agent). - Label `traefik.http.routers..entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}` gehört dazu; sultan prüft Entrypoint-/Resolver-Namen nicht. - `loadbalancer.server.port` = Port **im** Container (der Agent rechnet auf den Host-Port um). - Namen: kein doppelter Unterstrich `__` (Trenner bei sultan). - Wartung: `docker compose stop` → Wartungsseite; `down` → 404. Für Updates `up -d` (ersetzt Container) ist unkritisch. ### 7.2 Stacks mit mehreren Services / Datenbank - Datenbanken als **eigener Stack** (`postgres`), nie veröffentlicht. Der Stack legt ein benanntes Netz an (`postgres_net`); Anwendungen hängen es als `external: true` an. Je Anwendung eigene Datenbank + eigener User. - Innerhalb eines Stacks: `depends_on` mit `condition: service_healthy`. - Anwendung braucht Internet/Port → zusätzlich Netz `default`. ### 7.3 `.env`: Erzeugung und Geheimnisse - `.env.example` ist die **einzige Quelle** der Schlüssel. Platzhalter: `@@SECRET@@` → beim ersten Deployment zufällig erzeugt (je Schlüssel eigener Wert); `<…>` → vom Betreiber zu setzen. - Die echte `.env` erzeugt Ansible **nur, wenn sie fehlt**; danach gehört sie dem Betreiber (kein Überschreiben). Später ersetzen Vault-Einträge die Werte (`rules/secrets.md`). - Werte, die mehrere Stacks brauchen (DB-Passwort), stehen in der `.env` der **Anwendung**; Ansible liest sie dort und legt damit DB/User an (über stdin, nie als Argument). ### 7.4 Deployment per Ansible - Rolle `compose_stack` (`ansible/roles/compose_stack`): Stack-Verzeichnis `//`, Dateien aus `stacks//`, Bind-Mount-Ziele mit Eigentümer/Rechten, `.env` (s. 7.3), `community.docker.docker_compose_v2` mit `wait`. - Datenbank je Anwendung: Rolle `pg_app_db`. ### 7.5 Rechte, Dateisystem - Bind-Mount-Ziele mit dem Eigentümer des Containers anlegen (z. B. `1000:1000`); im Stack-README dokumentieren. - Auf btrfs: Datenbank-Verzeichnisse vor dem ersten Start `chattr +C` (kein Copy-on-Write, weniger Fragmentierung; Prüfsummen entfallen dort). ### 7.6 Backup & Update - Datenbanken per Dump sichern (`pg_dumpall`/`pg_dump` vor dem Borgmatic-Lauf) – das reine Kopieren des Datenverzeichnisses im Betrieb ist kein konsistentes Backup. - Update: neuen Tag in `.env` → Backup → `docker compose pull && docker compose up -d`. Rückweg: alter Tag + Restore. **Kein automatisches `docker system prune -a`** – das alte Image bleibt für den schnellen Rückweg. ## Festgelegte Werte ``, `TRAEFIK_NETWORK`, `TRAEFIK_CERTRESOLVER`, `GELF_HOST`/`GELF_PORT` stehen in [`ai/context/environment.md`](../context/environment.md). ## Änderungsprotokoll - 07.10.2026 – übernommen und verallgemeinert (Mandantenwerte → Platzhalter, Header-Pflicht ergänzt). - 23.10.2026 – Mandantenbezug entfernt, Werte nur noch in `environment.md`; Verweis auf `web_search.md` korrigiert. - 25.10.2026 – § 7: sultan-Variante, Mehrfach-Services/DB, `.env`-Erzeugung (`@@SECRET@@`), Deployment per Ansible, Rechte/btrfs, Backup/Update ohne Prune – Entscheidungen des Betreibers vom 23.–25.10.2026.