9.4 KiB
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:
sudo chown -R 1000:1000 data
Verwendete Umgebungsvariablen:
PORT– API-Port, Standard3000API_INTERNAL_URL– internes API-Ziel des Next.js-Rewrites, im Compose-Netzhttp://api:3000NEXT_TELEMETRY_DISABLED=1CHOKIDAR_USEPOLLING=trueundWATCHPACK_POLLING=true– nur incompose.dev.yaml, für Dateibeobachtung über Bind-Mounts hinwegLOG_LEVEL– steuert für beide Dienste die Ausgabestufe des strukturierten JSON-Loggers (error,warn,info,verbose,debug), Standardinfo. Setzbar über eine.env-Datei nebencompose.yamloderLOG_LEVEL=verbose docker compose up.AUTH_BOOTSTRAP_USERNAMEundAUTH_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 HTTPStrue; beim direkten HTTP-Zugriff im LANfalse, 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:
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.
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.