Files
watermaps/deploy/README.md
T
BuTzZ 99ba1a2373
Test and publish container images / test (push) Successful in 2m26s
Test and publish container images / publish (push) Successful in 1m50s
Make first deployment race-safe
2026-07-29 13:18:44 +02:00

272 lines
12 KiB
Markdown

# Watermaps-Produktion
Dieser Stack hostet die Watermaps-App, die lokalen Fahrrouten sowie eine
persistente PostGIS-Datenbank für Häfen, Schleusen, Brücken und Kontaktdaten in
Deutschland und den Niederlanden. Die sichtbaren Kartenkacheln bleiben externe
Dienste. Öffentlich gebunden werden ausschließlich TCP 80 und 443; App und
PostGIS sind nur im internen Docker-Netz erreichbar.
## Konfiguration
```bash
cp deploy/.env.production.example deploy/.env.production
editor deploy/.env.production
```
Mindestens `WATERMAPS_ACME_EMAIL` und `WATERMAPS_POSTGRES_PASSWORD` müssen
angepasst werden. Für das Datenbankpasswort eignet sich ein URL-unabhängiges
Hex-Secret:
```bash
openssl rand -hex 32
```
Außerdem müssen `WATERMAPS_REGISTRY` und `WATERMAPS_REGISTRY_OWNER` auf die
Gitea Container Registry zeigen. Der Hetzner-API-Token gehört **nicht** in
diese Datei. Er bleibt lokal in der ignorierten Datei
`infra/opentofu/terraform.tfvars` (alternativ kann der Provider
`TF_VAR_hcloud_token` lesen).
Die private SSH-Keydatei wird ebenfalls nicht gespeichert. Standardmäßig
verwenden die lokalen Deployment-Skripte
`~/.ssh/watermaps_deploy_ed25519`; alternativ kann sie mit `--identity` oder
über `WATERMAPS_SSH_KEY` angegeben werden. Der vorbereitete SSH-Benutzer heißt
standardmäßig `deploy`; die privilegierten Installationsschritte laufen über
dessen passwortloses `sudo`.
## Gitea Actions und Container Registry
Der Workflow `.gitea/workflows/container-images.yml` führt Typechecks und Tests
aus. Nach einem erfolgreichen Push auf `main` baut und veröffentlicht er drei
OCI-Images unter der vollständigen Git-Commit-SHA:
```text
gitea.incoso.eu/kevin_janssen/watermaps:<commit-sha>
gitea.incoso.eu/kevin_janssen/watermaps-route-data:<commit-sha>
gitea.incoso.eu/kevin_janssen/watermaps-release:<commit-sha>
```
Das dritte Image ist ein nicht ausführbares Release-Bundle mit Compose-Datei,
Nginx-Konfiguration, Systemd-Units, Deployment-Skripten und versionierten
Datenbankmigrationen. Sein geprüftes Manifest bindet die exakten
Registry-Digests von App und Routingdaten; `SHA256SUMS` schützt alle Dateien des
Bundles. Erst nachdem alle drei Commit-Images veröffentlicht wurden, wird
`watermaps-release:main` als einzelner atomarer Release-Pointer verschoben.
App- und Routingdaten-Images werden nie über bewegliche Tags gestartet.
In Gitea müssen Repository Actions aktiviert und ein Docker-fähiger
`ubuntu-latest`-Runner registriert sein. Unter
`Repository → Settings → Actions → Secrets` werden benötigt:
- `REGISTRY_USERNAME`: Gitea-Benutzer, dem die Packages gehören
- `REGISTRY_TOKEN`: eigener CI-Personal-Access-Token ausschließlich mit
`write:package`
Der separate PAT ist nötig, weil Gitea 1.26 mit dem eingebauten Job-Token noch
keine OCI-Pakete veröffentlichen kann. Für Pull Requests werden nur Tests
ausgeführt; Registry-Secrets werden dabei nicht verwendet.
Die Secrets lassen sich mit zwei getrennten, lokal ignorierten Token-Dateien
konfigurieren:
- `deploy/.gitea-actions-config-token`: kurzlebiger Konfigurations-PAT nur mit
`write:repository`, um die Actions-Secrets über die Repository-API zu setzen
- `deploy/.gitea-registry-publish-token`: CI-PAT nur mit `write:package`, der
anschließend als `REGISTRY_TOKEN` in Gitea Actions hinterlegt wird
Beide Dateien enthalten jeweils nur den Token in einer einzelnen Zeile und
müssen exakt Dateimodus 600 haben:
```bash
chmod 600 \
deploy/.gitea-actions-config-token \
deploy/.gitea-registry-publish-token
./deploy/scripts/configure-gitea-actions.sh
```
Das Skript nutzt den Konfigurations-PAT ausschließlich lokal für den API-Aufruf
und speichert ihn ausdrücklich nicht als Actions-Secret. Anschließend sollten
beide lokalen Token-Dateien gelöscht werden; Gitea zeigt den gespeicherten
Registry-Secret-Wert nicht wieder an.
## Deployment
Nach einem erfolgreichen Image-Build liest das Skript standardmäßig den
OpenTofu-Output `server_ipv4`. Es erlaubt ausschließlich einen sauberen,
vollständig committeten Git-Stand und überträgt für die Ersteinrichtung nur
Bootstrap-Dateien und die getrennte Produktionskonfiguration, nicht den
Anwendungsquellcode. Der Server bezieht das kanonische Release anschließend
selbst aus der Gitea Registry und prüft, dass es exakt `git rev-parse HEAD`
entspricht.
Pakete des öffentlichen Gitea-Benutzers können ohne Server-Credential gelesen
werden. Wird die Registry später privat geschaltet, werden einmalig
beziehungsweise nach Tokenwechsel lokale Pull-Zugangsdaten mitgegeben:
```bash
WATERMAPS_REGISTRY_USERNAME=kevin_janssen \
WATERMAPS_REGISTRY_TOKEN='<package-read-token>' \
./deploy/scripts/upload-and-deploy.sh \
--identity ~/.ssh/watermaps_deploy_ed25519
```
Der Token benötigt auf dem Produktionsserver nur `package: Read`. Er wird per
SSH an `docker login --password-stdin` übergeben, nicht in
`.env.production`, einem Container oder dem Image gespeichert. Docker legt die
Anmeldung root-lesbar in seiner lokalen Client-Konfiguration ab. Bei späteren
Deployments muss der Token nicht erneut angegeben werden, solange die
Anmeldung gültig ist:
```bash
./deploy/scripts/upload-and-deploy.sh \
--identity ~/.ssh/watermaps_deploy_ed25519
```
Das Serverskript lädt das Release-Manifest, App, den kombinierten
Routing-/Feature-Daten-Builder, PostGIS, Nginx und Certbot. App und Builder
werden ausschließlich über die im Release gebundenen Digests gestartet.
Geordnete SQL-Dateien unter `database/migrations/` werden mit
`watermaps_schema_migrations` protokolliert und jeweils in einer Transaktion
ausgeführt. Neue Migrationen müssen nach dem Expand/Contract-Prinzip mit dem
vorherigen App-Release kompatibel bleiben. Der Produktionsserver benötigt
weder Git noch Node/npm oder einen Repository-Checkout.
Der erste Datenaufbau lädt die Geofabrik-Extrakte für Deutschland und die
Niederlande, baut den Fahrroutenindex und importiert daraus die
`marine_features`. Je nach Serverleistung kann dies längere Zeit dauern.
Der Erstlauf wird dafür als begrenzter systemd-Job gestartet; das lokale
Upload-Skript wartet über wiederverbindbare SSH-Prüfungen auf exakt den
angeforderten Commit. So konkurriert der initiale Aufruf nicht mit dem
Auto-Deploy-Timer und ein kurzzeitiger SSH-Abbruch beendet den Import nicht.
App und Nginx werden beim ersten Release erst gestartet, wenn sowohl der
Routingindex als auch ein nicht leerer, zur PBF-Prüfsumme passender Bestand an
Häfen, Schleusen und Brücken geprüft wurde. Ein HTTP-Healthcheck allein kann
damit keine leere Ereignisdatenbank freigeben.
Die produktiven Daten liegen auf dem persistenten Servervolume:
```text
/srv/watermaps-data/local/germany-netherlands-fairways.json
/srv/watermaps-data/local/.marine-features.ready
/srv/watermaps-data/postgres/
/srv/watermaps-data/tmp/
```
PostGIS veröffentlicht keinen Host-Port. Die App verbindet sich im privaten
Compose-Netz; das Passwort wird separat über `PGPASSWORD` übergeben und muss
nicht URL-kodiert in `DATABASE_URL` dupliziert werden. Auch die temporären,
potenziell mehrere Gigabyte großen Filter- und GeoJSON-Dateien des Imports
liegen unter `tmp/` auf diesem Volume und nicht im Docker-Overlay der
Rootdisk; nach jedem Importlauf werden sie entfernt.
Der Upload wartet zuerst auf SSH und den Abschluss von Cloud-init. Anschließend
startet und prüft er `watermaps-volume-setup.service`. Ohne tatsächlich unter
`/srv/watermaps-data` eingehängtes Volume wird kein Download gestartet, damit
die großen PBF-Dateien nicht versehentlich auf dem Root-Dateisystem landen.
Für den kurzzeitigen Speicherpeak beim kombinierten Indexaufbau richtet der
Bootstrap zusätzlich die über `WATERMAPS_SWAP_SIZE_GB` konfigurierte,
persistente Swap-Reserve ein.
Vor dem Livegang antwortet Nginx nur für ACME-Challenges. Alle anderen
HTTP-Anfragen erhalten 404.
## Rollback
Jedes erfolgreiche Release wird mit Commit-SHA und den aufgelösten
Image-Digests unter `/srv/watermaps-runtime/deployments/` gespeichert; sein
vollständiges Bundle liegt unter `/srv/watermaps-data/releases/<commit>`.
`/opt/watermaps/current` wird erst nach Health-, Routen- und Featuretests
atomar auf ein neues Bundle umgeschaltet. Scheitert ein Deployment, wird das
vorherige unveränderliche Bundle erneut ausgeführt und vollständig geprüft.
Die persistente PostGIS-Datenbank bleibt erhalten; Migrationen sind deshalb
bewusst vorwärtskompatibel zu gestalten.
Das unmittelbar vorherige Release lässt sich auch manuell aktivieren:
```bash
./deploy/scripts/remote-rollback.sh \
--identity ~/.ssh/watermaps_deploy_ed25519
```
Oder ein bestimmter, bereits erfolgreich deployter Commit:
```bash
./deploy/scripts/remote-rollback.sh \
--revision 0123456789abcdef0123456789abcdef01234567 \
--identity ~/.ssh/watermaps_deploy_ed25519
```
Der Rollback verwendet gespeicherte Digests, nicht einen beweglichen Tag.
## DNS und SSL-Livegang
Sobald der OpenTofu-Output bekannt ist, kann folgender DNS-Eintrag gesetzt
werden:
```text
A watermaps.incoso.eu <server_ipv4>
```
Nach der DNS-Propagation wird der Livegang lokal ausgelöst:
```bash
./deploy/scripts/remote-go-live.sh --identity ~/.ssh/watermaps_deploy_ed25519
```
Das Serverskript prüft, dass sämtliche A-Records ausschließlich auf die
erwartete IPv4 zeigen, testet den Routingindex mit je einer Route in
Deutschland und den Niederlanden, prüft den ACME-Webroot, fordert das
Zertifikat an und aktiviert erst anschließend HTTPS. HTTP leitet danach auf
HTTPS um.
## Automatik
`bootstrap-server.sh` installiert drei systemd-Timer:
- `watermaps-auto-deploy.timer`: prüft ausgehend jede Minute
`watermaps-release:main`. Das Revision-Label muss exakt dem aktuellen
Gitea-`main`-Commit entsprechen. Der Server prüft Manifest, Dateiprüfsummen,
gebundene Image-Digests und OCI-Revisionen und deployt erst nach einer
zweiten Pointer-/Branch-Prüfung. Es ist weder ein eingehender Gitea-Webhook
noch ein SSH-Key im Actions-Runner nötig. Aktives, vorheriges und die über
`WATERMAPS_RELEASE_RETENTION` festgelegte Zahl lokaler Releases bleiben
erhalten; ältere Watermaps-Artefakte werden gezielt bereinigt. Vor dem ersten
Release verwendet ein stabiler Systemd-Einstiegspunkt das Bootstrap-Skript,
danach automatisch das atomar aktivierte `current`. Noch nicht fertige erste
Registry-Builds werden dadurch zuverlässig erneut versucht.
- `watermaps-route-update.timer`: täglich neue Deutschland- und
Niederlande-Daten. Der Fahrroutenindex wird atomar aktiviert. Der
Feature-Importer aktualisiert beide Länder per Upsert und entfernt
verschwundene Datensätze erst, nachdem beide Importe vollständig waren;
dabei werden ausschließlich OSM-Zeilen bereinigt. EuRIS- und
Website-Anreicherungen bleiben erhalten.
- `watermaps-certbot-renew.timer`: zweimal täglich Certbot-Prüfung mit
anschließendem Nginx-Reload.
Ein vollständiger manueller Feature-Neuimport lässt sich über die
Option `--force-marine` erzwingen:
```bash
/opt/watermaps/current/deploy/scripts/update-route-data.sh --force-marine
```
`WATERMAPS_REBUILD_MARINE_DATA=true` in `.env.production` erzwingt den
Neuimport stattdessen beim nächsten regulären Deployment. Der systemd-Dienst
hat bewusst kein Startzeitlimit, da Download, Indexbau und Import auf kleineren
Servern deutlich länger als 90 Sekunden dauern können.
Status und Logs:
```bash
systemctl list-timers 'watermaps-*'
journalctl -u watermaps-auto-deploy.service
journalctl -u watermaps-route-update.service
journalctl -u watermaps-certbot-renew.service
docker compose \
--project-directory /opt/watermaps/current \
--env-file /opt/watermaps/deploy/.env.production \
--env-file /opt/watermaps/deploy/.env.images \
-f /opt/watermaps/current/deploy/compose.production.yml ps
```