186 lines
9.4 KiB
Markdown
186 lines
9.4 KiB
Markdown
# 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=<eigenes Startpasswort mit mindestens 12 Zeichen>
|
||
```
|
||
|
||
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/<container-id>/` 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.
|