# Deployment und Betrieb ## Aktueller Status Es gibt zwei getrennte Compose-Dateien. Sie dürfen nicht verwechselt werden. | Datei | Projektname | Zweck | | --- | --- | --- | | `compose.yaml` | `leistungsbilanz-dev` | ausschließlich lokale Entwicklung | | `compose.prod.yaml` | `leistungsbilanz` | Dauerbetrieb im LAN | `compose.yaml` startet `tsx watch` und `next dev` und bindet Quellcode vom Host ein. Dieser Stack ist **nicht** für Dauerbetrieb geeignet (siehe [Warum kein Dev-Stack im Dauerbetrieb](#warum-kein-dev-stack-im-dauerbetrieb)). Seine Ports sind deshalb an `127.0.0.1` gebunden. `compose.prod.yaml` startet kompilierten Code ohne Dateibeobachter. Es enthält weiterhin weder TLS, Authentifizierung noch ein Benutzer-/Rollenmodell; die Express-API wird deshalb nicht auf dem Host veröffentlicht, sondern nur über den Next.js-Rewrite erreicht. Der Stack gehört hinter einen Reverse Proxy mit Authentifizierung und darf nicht öffentlich erreichbar gemacht werden. ## Topologie Entwicklung (`compose.yaml`): | Komponente | Port | Healthcheck | Speicherlimit | Persistenz | | --- | ---: | --- | ---: | --- | | Next.js Web | `127.0.0.1:3001` | `GET /health` alle 30 s | 2 GB | keine | | Express API | `127.0.0.1:3000` | `GET /health` alle 30 s | 1 GB | `./data:/app/data` | Produktion (`compose.prod.yaml`): | Komponente | Port | Healthcheck | Speicherlimit | Persistenz | | --- | ---: | --- | ---: | --- | | Next.js Web | `3090` | `GET /health` alle 30 s | 1 GB, kein Swap | keine | | Express API | nur intern | `GET /health` alle 30 s | 512 MB, kein Swap | Volume `leistungsbilanz-data` | | SQLite | Datei | Integritäts-/FK-Prüfung via Backup und Skript | – | `data/leistungsbilanz.db` | 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` - `NODE_ENV=production` – nur in `compose.prod.yaml` - `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`. Beim API-Start laufen in der Entwicklung zuerst `npm run db:migrate` und `npm run db:verify:circuit-schema` (drizzle-kit, eine devDependency). Das Produktionsimage enthält keine devDependencies und migriert stattdessen über `node scripts/run-migrations.js`, das denselben Migrationsordner mit `drizzle-orm` anwendet. ## Warum kein Dev-Stack im Dauerbetrieb `compose.yaml` lief einmal fünf Tage durchgehend auf einem Server. Ergebnis: 10,3 GB belegter Arbeitsspeicher im Web-Container, 12,2 % Dauer-CPU im API-Container und ein Host, der 13 GB Swap belegt hatte. Vier Ursachen wirkten zusammen; alle vier sind inzwischen behoben: 1. **Polling-Dateibeobachter.** `CHOKIDAR_USEPOLLING` und `WATCHPACK_POLLING` ließen `tsx watch` permanent den Quellbaum abklappern. Beide Variablen sind entfernt; unter Linux funktioniert `inotify` auf Bind-Mounts. 2. **Healthcheck als Seitenrendering.** Der Web-Healthcheck rief `/` alle fünf Sekunden auf. `/` ist ein `redirect("/projects")`, und Node's `fetch` folgt Redirects – jede Prüfung rendert also die vollständige Projektliste, rund 16.000-mal pro Tag. Der Healthcheck zeigt jetzt auf `/health` und läuft alle 30 Sekunden. 3. **Kein Speicherlimit.** `next dev` hält Kompilierungs-State und wächst unter Dauerlast unbegrenzt. Beide Compose-Dateien setzen jetzt `mem_limit`; in der Produktion zusätzlich `memswap_limit` in gleicher Höhe, damit ein Leck den Container beendet statt den Host in den Swap zu ziehen. 4. **Kollidierender Projektname.** `compose.yaml` hieß `leistungsbilanz` und damit genauso wie das Produktionsprojekt; ein `docker compose down` im Entwicklungsverzeichnis zielte auf den Produktionsstack. Der Entwicklungsname ist jetzt `leistungsbilanz-dev`. ## Produktionsdeployment ```bash docker compose -f compose.prod.yaml up --build --detach docker compose -f compose.prod.yaml ps docker compose -f compose.prod.yaml logs --follow ``` Das Frontend hört danach auf Port 3090. Die API ist nur innerhalb des Compose-Netzes erreichbar. Das Produktionsimage läuft als Benutzer `node` statt als `root`. Wenn ein bestehendes Datenvolume von einem früheren root-Container beschrieben wurde, müssen dessen Dateien einmalig übereignet werden, sonst schlagen Schreibzugriffe mit `SQLITE_READONLY` fehl: ```bash docker run --rm -v leistungsbilanz_leistungsbilanz-data:/data alpine \ chown -R 1000:1000 /data ``` Ein bestehendes Ein-Container-Deployment über `scripts/docker-start.sh` wird abgelöst, indem dessen Stack gestoppt und `compose.prod.yaml` mit demselben Projektnamen `leistungsbilanz` gestartet wird; das Volume bleibt dabei erhalten. ## 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. Anfragen von Healthcheck- und Monitoring-Clients (`node`, `curl`, `wget`, …) werden anhand des User-Agents aus dem Navigationslog gefiltert und `/health` läuft gar nicht erst durch `src/proxy.ts`. ## Herunterfahren `SIGTERM` und `SIGINT` schließen den HTTP-Server, danach das SQLite-Handle, und beenden den Prozess mit Code 0. Kommt der Server binnen 15 Sekunden nicht herunter, beendet sich der Prozess trotzdem. Vorher wurde das Signal nur geloggt: jeder `docker stop` lief in die Grace Period und endete mit `SIGKILL`, möglicherweise mitten in einem Schreibvorgang. `stop_grace_period` steht in beiden Compose-Dateien auf 20 s und liegt damit über dem internen Timeout. ## Offene Punkte für einen vollwertigen Produktionsbetrieb Umgesetzt: - reproduzierbares Produktionsimage und Next.js-Produktionsstart - kontrollierter Migrationsschritt vor dem API-Start - strukturierte Logs mit Rotation, Healthchecks, Speicherlimits - definiertes Herunterfahren Weiterhin offen: - TLS-Termination und Reverse Proxy - Authentifizierung, Autorisierung und Benutzer-/Rollenmodell - definierte Secrets- und Konfigurationsverwaltung - Alarmierung auf Basis der Healthchecks - getestete Backup-, Restore- und Rollback-Prozeduren - Entscheidung, ob SQLite für einen einzelnen Prozess genügt oder PostgreSQL für Mehrbenutzerbetrieb erforderlich ist Solange Authentifizierung fehlt, gehört der Stack ausschließlich in ein vertrauenswürdiges Netz oder hinter einen authentifizierenden Reverse Proxy. ## 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 zukünftigen logischen Projektversionen getrennt. 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.