Regeln, Standards, Kontextstruktur und Werkzeuge übernommen; 2nd-brain- Inhalte (Stacks, Ansible-Inventar, Architektur, Knowledge) entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
11 KiB
Markdown
190 lines
11 KiB
Markdown
# 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}_<service>`) | 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` (`${<APP>_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
|
||
```
|
||
<STACK_BASE>/<stack>/ # <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: <hersteller>/<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}:<intern>"
|
||
extra_hosts: # nur wenn benötigt; ein Eintrag je Variable
|
||
- "${EXTRA_HOST_1}"
|
||
volumes:
|
||
- ${DIR_DATA}:/<pfad-im-container>
|
||
- ${DIR_CONFIG}:/<config-pfad>: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:<intern>/<pfad>"]
|
||
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 `<stack>.invalid` (reservierte TLD) verhindert, dass ein vergessener Wert je eine echte Domain anfragt.
|
||
- Mehrere Web-Services: Router-/Service-Namen eindeutig halten (`${COMPOSE_PROJECT_NAME}_<service>`).
|
||
|
||
### 3.3 `.env` / `.env.example`
|
||
```dotenv
|
||
# --- Stack ---
|
||
COMPOSE_PROJECT_NAME=<stack> # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag)
|
||
APP_TAG=<version>
|
||
|
||
# --- Logging (Graylog) ---
|
||
GELF_HOST=<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.domain.tld>
|
||
APP_INTERNAL_PORT=<port>
|
||
#TRAEFIK_NETWORK=traefik-net
|
||
#TRAEFIK_CERTRESOLVER=le
|
||
|
||
# --- Hosts ---
|
||
EXTRA_HOST_1=<fqdn>:<ip>
|
||
|
||
# --- 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 <image> -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}:<intern>"]` (Bindung `0.0.0.0`; `127.0.0.1` verwirft der sultan-Agent).
|
||
- Label `traefik.http.routers.<name>.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 `<STACK_BASE>/<stack>/`, Dateien aus `stacks/<stack>/`,
|
||
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
|
||
`<STACK_BASE>`, `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.
|