# Deployment — desfoto.de ## Zielumgebung | Baustein | Wert | | --- | --- | | Produktionshost | `prod-main` = `opencode-prod@87.106.24.138` (NOPASSWD sudo) | | Live-Verzeichnis | `/home/denny/stacks/desfoto/` (Owner `denny:denny`) | | Release-Snapshots | `/home/denny/stacks/desfoto-releases/-/` | | Compose-Dateien | `compose.yml` + `compose.vps.yml`, Projektname `desfoto` | | Container | `desfoto-web-1` (`nginx:1.28-alpine`), gebunden an `127.0.0.1:18430` | | Netzwerk | externes Docker-Netzwerk `web` (Traefik-Docker-Provider) | | Reverse Proxy | Traefik v3 im Projekt `stack` (`/srv/stack/docker-compose.yml`) | | Entrypoints | `http` und `https`, ACME-Resolver `le`, HTTP-01-Challenge | | Zertifikate | `/srv/traefik/acme.json` | | Domains | `desfoto.de` (kanonisch) und `www.desfoto.de` → 301 auf Apex | Der Proxy wird **niemals** neu gestartet oder neu erzeugt. Die Anbindung erfolgt ausschließlich über Labels in `compose.vps.yml`. ## Ablauf einer Veröffentlichung ```bash /home/king/bin/oc-release /home/king/projects/desfoto \ "feat: ..." ``` `oc-release` committet und pusht die genannten Dateien und ruft danach die getrackten Hooks auf: 1. `.ocauto/deploy ` - prüft, dass `HEAD` dem Release entspricht und `site/`, `nginx.conf`, `compose.yml`, `compose.vps.yml` keine uncommitteten Änderungen haben; - packt diese Pfade in ein Archiv und lädt es nach `prod-main:/tmp`; - legt auf dem Server zuerst einen Snapshot der aktuell laufenden Version an (`/home/denny/stacks/desfoto-releases/-/`, zusätzlich als `previous` verlinkt); - entpackt die neue Version nach `/home/denny/stacks/desfoto/`, setzt den Owner auf `denny:denny` und schreibt die Release-Kennung nach `RELEASE`; - `docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans` und wartet auf `healthy`; - entfernt einmalig die veralteten `desfoto.de`-Weiterleitungs-Labels aus `/srv/stack/docker-compose.yml`: Es werden ausschließlich Traefik-Label-Zeilen mit `desfoto`-Bezug sowie der zugehörige Kommentar entfernt, das Ergebnis wird zuerst als Compose-Projekt validiert (`config --quiet`) und danach per atomarem `mv` an die Stelle der Live-Datei gesetzt. Eine Vorher-Fassung liegt im Snapshot (`docker-compose.yml.stack-backup`) und daneben als `docker-compose.yml.bak.`. Danach wird ausschließlich der `landing`-Container mit `--no-deps` neu erzeugt, damit `dennyapp.de` und `dennyschulz.de` unverändert weiterlaufen. Bleibt eine `desfoto`-Referenz übrig (z. B. ein eigener Service-Block), wird sie als WARNUNG ausgegeben. 2. `.ocauto/verify ` - vergleicht den Dateibaum unter `/home/denny/stacks/desfoto/site` mit dem committeten `site/` (SHA-256 über alle Dateien außer den datierten `sitemap.xml`/`security.txt`); - prüft alle 13 Routen, die 404-Seite, `robots.txt`, `sitemap.xml`, `.well-known/security.txt`, Bilder und Schriften; - prüft die Weiterleitungen `http → https` und `www → Apex`; - prüft die Sicherheits-Header, dass kein `Set-Cookie` gesetzt wird und dass das Zertifikat noch mindestens sieben Tage gültig ist; - prüft, dass `www.dennyschulz.de` und `dennyapp.de` weiterhin erreichbar sind. ## Routing `desfoto.de` wird vor dem Aufräumen bereits vom neuen Container bedient, weil die Routers in `compose.vps.yml` eine explizite Traefik-Priorität `200` tragen und die alten Weiterleitungsrouters keine Priorität setzen (damit gilt dort die Regel-Länge). Der Container wird also zuerst gesund geprüft, und erst danach werden die alten Labels entfernt — der Übergang hat damit kein Fenster ohne Antwort. `www.desfoto.de` wird über die Middleware `desfoto-canonical` dauerhaft auf `https://desfoto.de/...` umgeschrieben. Für `http` bringt die Anwendung einen eigenen Router (`desfoto-http` + `desfoto-http-to-https`) mit, der dauerhaft (301) auf `https` umleitet. Die ACME-HTTP-01-Challenge beantwortet Traefik über den internen `acme-http`-Router mit maximaler Priorität, bevor dieser Redirect greift. ### Abweichung der laufenden Traefik-Konfiguration (Live-Drift) `/srv/stack/docker-compose.yml` deklariert eine globale Entrypoint-Weiterleitung `http → https` (`--entrypoints.http.http.redirections.entryPoint.to=https` / `…scheme=https`, Zeilen 10–11). Der **laufende** Container `traefik` führt diese Flags nachweislich nicht (`docker inspect traefik --format '{{json .Args}}'` enthält keine `redirections`-Flags; `http://dennyapp.de/` leitet nur deshalb um, weil `landing` einen eigenen `landing-http`-Router besitzt). Die Datei auf der Platte und die laufende Konfiguration stimmen also nicht überein. Der Proxy wird deswegen **nicht** aus der Datei neu erzeugt oder neu gestartet: Das würde alle fremden Routen risikobehaftet anfassen. Stattdessen bringt jede Anwendung ihr eigenes HTTP-Routing mit — `desfoto.de` über die Labels in `compose.vps.yml`. Wird die globale Weiterleitung später bewusst aktiviert, bleibt der eigene Router schlicht wirkungslos. Nachzuprüfen mit: ```bash curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' http://desfoto.de/ # erwartet: 301 https://desfoto.de/ ``` ## Rollback ```bash ROLLBACK_TO=/home/denny/stacks/desfoto-releases/- \ /home/king/projects/desfoto/.ocauto/deploy rollback ``` Der Befehl synchronisiert `site/`, `nginx.conf` und beide Compose-Dateien aus dem Snapshot zurück nach `/home/denny/stacks/desfoto/` und startet den Stack neu. Die frühere Version bleibt so lange verfügbar, bis sie bewusst gelöscht wird. Jeder Release legt den Snapshot an, auch der allererste. Enthält der Snapshot noch kein `site/` (weil vorher nichts ausgeliefert wurde), entfernt der Rollback den desfoto-Stack wieder und stellt den Zustand vor der Veröffentlichung her. Wurde `/srv/stack/docker-compose.yml` verändert, liegt die Vorher-Fassung sowohl im Snapshot als `docker-compose.yml.stack-backup` als auch daneben unter `docker-compose.yml.bak.`. Beide werden vom Rollback automatisch zurückgespielt, gefolgt von `docker compose up -d --no-deps landing`; das Backup `.bak.` bleibt zusätzlich für einen manuellen Eingriff liegen. ## Betrieb ```bash ssh prod-main sudo docker compose -f /home/denny/stacks/desfoto/compose.yml \ -f /home/denny/stacks/desfoto/compose.vps.yml ps sudo docker logs --tail 50 desfoto-web-1 sudo docker inspect --format '{{.State.Health.Status}}' desfoto-web-1 ``` Access-Logs gibt es bewusst nicht (`access_log off` in `nginx.conf`). Auch Fehler schreibt nginx nicht weg: `error_log /dev/null crit;` verwirft sie vollständig, statt sie in eine Datei zu schreiben. Der JSON-Log-Treiber des Containers ist zusätzlich auf 5 MB × 2 Dateien begrenzt. Das entspricht der Zusage in der Datenschutzerklärung.