# 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. Sie kann beim Deployment mit `--identity` oder über `WATERMAPS_SSH_KEY` angegeben werden. Falls OpenTofu keinen Output `ssh_private_key_path` bereitstellt und der Key nicht bereits über den SSH-Agenten verfügbar ist, ist eine dieser beiden Angaben erforderlich. 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 zwei OCI-Images unter der vollständigen Git-Commit-SHA: ```text gitea.incoso.eu/kevin_janssen/watermaps: gitea.incoso.eu/kevin_janssen/watermaps-route-data: ``` 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`: Personal Access Token mit `package: Read and Write` Der separate PAT ist derzeit nötig, weil Giteas eingebauter Job-Token OCI-Pakete noch nicht zuverlässig veröffentlichen kann. Für Pull Requests werden nur Tests ausgeführt; Registry-Secrets werden dabei nicht verwendet. ## 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 nur den kleinen Ordner `deploy/` sowie das kanonische `database/schema.sql`, nicht den Anwendungsquellcode. Die Image-Tags entsprechen exakt `git rev-parse HEAD`. Ist die Registry privat, werden einmalig beziehungsweise nach Tokenwechsel lokale Pull-Zugangsdaten mitgegeben: ```bash WATERMAPS_REGISTRY_USERNAME=kevin_janssen \ WATERMAPS_REGISTRY_TOKEN='' \ ./deploy/scripts/upload-and-deploy.sh \ --identity ~/.ssh/watermaps_hetzner_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_hetzner_ed25519 ``` Das Serverskript lädt App, den kombinierten Routing-/Feature-Daten-Builder, PostGIS, Nginx und Certbot mit `docker compose pull`. Die beiden commitgenauen Gitea-Tags werden anschließend in ihre unveränderlichen Registry-Digests aufgelöst. Erst danach werden die Container mit `--no-build` gestartet. Der Produktionsserver benötigt deshalb weder Git noch Node/npm oder den Quellcode. 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. 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. Scheitert ein Deployment nach dem Containerwechsel, startet `deploy.sh` automatisch das vorherige Release und führt die Health-, Routen- und Featuretests erneut aus. Ein Rollback wechselt nur die unveränderlichen App-/Builder-Images. Die persistente PostGIS-Datenbank bleibt erhalten; das Schema und der Import sind aufwärtskompatibel und idempotent. Das unmittelbar vorherige Release lässt sich auch manuell aktivieren: ```bash ./deploy/scripts/remote-rollback.sh \ --identity ~/.ssh/watermaps_hetzner_ed25519 ``` Oder ein bestimmter, bereits erfolgreich deployter Commit: ```bash ./deploy/scripts/remote-rollback.sh \ --revision 0123456789abcdef0123456789abcdef01234567 \ --identity ~/.ssh/watermaps_hetzner_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 ``` Nach der DNS-Propagation wird der Livegang lokal ausgelöst: ```bash ./deploy/scripts/remote-go-live.sh --identity ~/.ssh/watermaps_hetzner_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 zwei systemd-Timer: - `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/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-route-update.service journalctl -u watermaps-certbot-renew.service docker compose \ --project-directory /opt/watermaps \ --env-file /opt/watermaps/deploy/.env.production \ --env-file /opt/watermaps/deploy/.env.images \ -f /opt/watermaps/deploy/compose.production.yml ps ```