# Deployment und Betrieb ## Aktueller Status Es gibt zwei Compose-Stacks. `compose.yaml` startet den gebauten Stand: `node dist/server/index.js` und `next start`, ohne Quellcode-Mounts und ohne Datei-Watcher. Das ist der Stack für einen Server. Beide Dienste laufen aus der `prod`-Stufe des Dockerfiles, also als Benutzer `node` statt als root, ohne devDependencies, mit `cap_drop: ALL`, `no-new-privileges` und harten Speichergrenzen. `compose.dev.yaml` startet `tsx watch` und `next dev` und bindet Quellcode vom Host ein. Die Watcher laufen im Polling-Modus, weil Bind-Mounts unter Windows und macOS keine inotify-Events durchreichen; das kostet dauerhaft CPU, auch ohne Benutzeraktivität. Dieser Stack gehört deshalb nur auf einen Entwicklungsrechner. Beide Stacks enthalten Anmeldung mit festen Rollen, aber weder TLS, einen vorgeschalteten Reverse Proxy noch ein zentral betriebenes Datenbanksystem. Passwörter und Session-Cookies werden bei reinem HTTP unverschlüsselt übertragen. Der Stack darf deshalb nicht öffentlich erreichbar gemacht werden. ## Topologie | Komponente | Port | Auf dem Host | Healthcheck | Persistenz | | --- | ---: | --- | --- | --- | | Next.js Web | 3001 | `3001` (Server) / `127.0.0.1:3001` (Dev) | `GET /web-health` | keine | | Express API | 3000 | nicht veröffentlicht (Server) / `127.0.0.1:3000` (Dev) | `GET /health` | `./data:/app/data` | | SQLite | Datei | – | Integritäts-/FK-Prüfung via Backup und Skript | `data/leistungsbilanz.db` | Die Express-API wird im Server-Stack nicht auf dem Host veröffentlicht (`expose` statt `ports`). Alle fachlichen API-Routen verlangen eine aktive Session; Schreibzugriffe zusätzlich den Anwendungsheader `X-Leistungsbilanz-Request: 1`. Erreichbar ist die API nur aus dem Compose-Netz, praktisch also über den Next.js-Rewrite. Im Entwicklungs-Stack hängen beide Ports am Loopback und sind nicht aus dem LAN erreichbar. Beim HTTP-Zugriff auf den Server-Stack aus dem LAN erzeugt der Editor neue UUIDs über `createClientId`, mit `crypto.getRandomValues()` als Ersatz für das dort nicht verfügbare `crypto.randomUUID()`. HTTPS ist für diese ID-Erzeugung nicht erforderlich. ### Speichergrenzen Jeder Dienst hat ein `mem_limit`. Im Server-Stack ist zusätzlich `memswap_limit` auf denselben Wert gesetzt, womit Swap für den Container abgeschaltet ist: ein Speicherleck lässt dann den Container am OOM-Killer sterben und `restart: unless-stopped` greift, statt dass der Host in den Swap läuft. Aktuell 512 MB für die API und 1 GB für das Frontend. Der Entwicklungs-Stack bekommt 1 GB bzw. 2 GB und behält Swap, weil `next dev` Kompilierzustand hält und im Dauerbetrieb stetig wächst. ### Rechte im Container Das Laufzeit-Image läuft als `node` (UID 1000). Ein `data/`-Verzeichnis, das ein früherer root-Container angelegt hat, ist dann nicht beschreibbar und braucht einmalig auf dem Host: ```bash sudo chown -R 1000:1000 data ``` Verwendete Umgebungsvariablen: - `PORT` – API-Port, Standard `3000` - `API_INTERNAL_URL` – internes API-Ziel des Next.js-Rewrites, im Compose-Netz `http://api:3000` - `NEXT_TELEMETRY_DISABLED=1` - `CHOKIDAR_USEPOLLING=true` und `WATCHPACK_POLLING=true` – nur in `compose.dev.yaml`, für Dateibeobachtung über Bind-Mounts hinweg - `LOG_LEVEL` – steuert für beide Dienste die Ausgabestufe des strukturierten JSON-Loggers (`error`, `warn`, `info`, `verbose`, `debug`), Standard `info`. Setzbar über eine `.env`-Datei neben `compose.yaml` oder `LOG_LEVEL=verbose docker compose up`. - `AUTH_BOOTSTRAP_USERNAME` und `AUTH_BOOTSTRAP_PASSWORT` – erzeugen nur bei leerer Benutzertabelle den ersten Administrator; das Startpasswort hat mindestens 12 Zeichen. - `AUTH_SESSION_HOURS` – Inaktivitätsgrenze einer Session, Standard 12 Stunden. - `AUTH_SESSION_MAX_DAYS` – absolute Session-Laufzeit, Standard 7 Tage. - `COOKIE_SECURE` – hinter HTTPS `true`; beim direkten HTTP-Zugriff im LAN `false`, da Browser das Cookie sonst nicht speichern. Vor dem ersten Start müssen mindestens die Bootstrap-Werte in einer lokalen, nicht versionierten `.env` neben `compose.yaml` gesetzt werden: ```dotenv AUTH_BOOTSTRAP_USERNAME=admin AUTH_BOOTSTRAP_PASSWORT= ``` Nach dem ersten erfolgreichen Start werden diese Werte ignoriert, solange mindestens ein Benutzer existiert. Weitere Zugänge legt ein Administrator in **Config → Benutzer** an. Details stehen in [Anmeldung und Benutzerverwaltung](authentication.md). Beim API-Start laufen zuerst die Migrationen und die Schemaprüfung (`scripts/run-migrations.js` und `scripts/db-verify-circuit-schema.js`, im Entwicklungsstack über `npm run db:migrate` und `npm run db:verify:circuit-schema`). `API_INTERNAL_URL` wirkt für `next start` zur **Build-Zeit**: `next build` schreibt die Rewrite-Ziele aus `next.config.mjs` fest in `.next/routes-manifest.json`. `compose.yaml` reicht den Wert deshalb als Build-Argument an das Image durch, nicht nur als Laufzeit-Variable. ## Logging Beide Dienste schreiben strukturierte, einzeilige JSON-Log-Zeilen nach stdout/stderr (`docker compose logs --follow`). Jede Zeile enthält `timestamp`, `level`, `scope` und `message`. `compose.yaml` konfiguriert für beide Dienste den `json-file`-Treiber mit Rotation (`max-size: 20m`, `max-file: 10`, also bis zu 200 MB je Dienst); ohne diese Einstellung würde Docker mit der Standardkonfiguration unbegrenzt in eine einzelne Datei unter `/var/lib/docker/containers//` schreiben. Die Logs überleben einen Container-Neustart (`docker compose restart`), aber nicht das Entfernen des Containers (`docker compose down` gefolgt von `up` erzeugt neue Container und damit neue, leere Logdateien); für ein echtes Langzeitarchiv über Rebuilds hinweg müssten die Zeilen zusätzlich in eine Datei im gemounteten `./data`-Verzeichnis oder an ein externes Log-System geschrieben werden. Die Express-API protokolliert jede abgeschlossene Anfrage (Methode, Pfad, Status, Dauer; `/health` wird nicht mitgeloggt) sowie unbehandelte Exceptions/Promise-Rejections. Das Next.js-Frontend protokolliert Seitenanfragen (Navigation) über `src/proxy.ts` und unbehandelte Fehler über `src/instrumentation.ts`. Beide Prozesse schreiben zusätzlich alle fünf Minuten einen `verbose`-Heartbeat mit Laufzeit und Speicherverbrauch – nützlich, um Speicherlecks oder Hänger vor einem 502 über einen längeren Zeitraum nachzuvollziehen. Für die Detailsuche `LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder API-Anfrage und macht damit hängende (nie abgeschlossene) Requests sichtbar. Ein `close`-Ereignis ohne vorheriges `finish` wird als `request aborted before response finished` (`warn`) geloggt und zeigt damit vom Client oder einem vorgeschalteten Proxy abgebrochene Verbindungen. Eine unbehandelte Exception oder Promise-Rejection wird geloggt und beendet den jeweiligen Prozess anschließend bewusst (`process.exit(1)`), statt in einem unbekannten Zustand weiterzulaufen. Beide Dienste laufen deshalb mit `restart: unless-stopped`, damit Docker sie danach automatisch neu startet; ohne diese Policy würde ein Crash den Dienst dauerhaft unerreichbar lassen. ## Herunterfahren `SIGTERM` und `SIGINT` schließen in der API den HTTP-Server und anschließend das SQLite-Handle und beenden den Prozess. Offene Keep-Alive-Verbindungen – vor allem die des Next.js-Servers – werden dabei über `server.closeIdleConnections()` freigegeben, laufende Requests dürfen zu Ende laufen. Vorher wurde das Signal nur geloggt, der Prozess lief weiter und Docker beendete ihn nach zehn Sekunden mit `SIGKILL`, mitten in einem möglichen Schreibvorgang. Greift der Abbau nicht innerhalb von 15 Sekunden, beendet sich der Prozess selbst mit Exit-Code 1; `stop_grace_period: 20s` in beiden Compose-Dateien liegt bewusst darüber. Die Startbefehle in `compose.yaml` enden mit `exec node …`. Ohne das `exec` bleibt `sh` der Vordergrundprozess, und `SIGTERM` erreicht Node nie. ## Voraussetzungen für ein späteres Produktionssetup Vor einer produktiven Installation werden mindestens benötigt: - TLS-Termination und Reverse Proxy - zentraler Identitätsanbieter oder eine feinere Rechte-Matrix, falls die zwei festen Rollen später nicht ausreichen - definierte Secrets- und Konfigurationsverwaltung - persistenter, gesicherter Datenbankbetrieb - kontrollierter einmaliger Migrationsschritt vor dem API-Rollout - Monitoring, strukturierte Logs und Alarmierung - getestete Backup-, Restore- und Rollback-Prozeduren - Entscheidung, ob SQLite für einen einzelnen Prozess genügt oder PostgreSQL für Mehrbenutzerbetrieb erforderlich ist Der in der README beschriebene Server-Stack ermöglicht bereits den Betrieb des gebauten Stands in einer geschützten Umgebung. Ein vollständig unterstütztes Produktionssetup bleibt bis zur Umsetzung und Prüfung dieser Punkte offen. ## Backup und Wiederherstellung `npm run db:backup` erzeugt unter `data/backups/` ein konsistentes SQLite-Backup, öffnet es unabhängig und prüft Integrität sowie Fremdschlüssel. Diese Dateisicherungen sind von den bereits vorhandenen benannten und automatischen logischen Projektsicherungen getrennt. Projektsicherungen werden über die Projektseite wiederhergestellt und unterstützen persistentes Undo/Redo. Eine produktive Restore-Anweisung wird erst zusammen mit einem unterstützten Produktionsbetrieb veröffentlicht. Lokale Wiederherstellung darf nur bei gestoppter API, gegen eine separate Datei und nach erneuter Integritätsprüfung erfolgen.