forked from jappel/leistungsbilanz-ts
Add production compose stack and stop idle load in containers
The development stack was running permanently on a server: polling file watchers, a healthcheck that rendered a full page every five seconds and no memory limit grew next dev to 10 GB and pushed the host into swap. - add compose.prod.yaml running compiled output in separate api/web services - make the Dockerfile multi-stage with dev and prod targets, prune devDependencies and run the runtime image as node instead of root - bake API_INTERNAL_URL at build time; next start ignores it at runtime because rewrite destinations are resolved into routes-manifest.json - drop CHOKIDAR_USEPOLLING and WATCHPACK_POLLING - probe /health instead of /, which redirects to /projects and made every healthcheck render the project list - give every service a memory limit and forbid swap in production - rename the development compose project to leistungsbilanz-dev so its down command cannot target the production stack - bind development ports to localhost - close the http server and the SQLite handle on SIGTERM/SIGINT - match probe user agents in the navigation log filter; Node's fetch sends one, so the previous check never matched - exit docker-start.sh when either supervised process dies - remove drizzle.config.js, a compiled copy drizzle-kit never reads, and the pre-Next index.html/styles.css leftovers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a17e2e3f4b
commit
01fa527b9c
14 changed files with 424 additions and 118 deletions
135
docs/deployment.md
Normal file → Executable file
135
docs/deployment.md
Normal file → Executable file
|
|
@ -2,21 +2,40 @@
|
|||
|
||||
## Aktueller Status
|
||||
|
||||
Es gibt derzeit kein unterstütztes Produktionsdeployment.
|
||||
Es gibt zwei getrennte Compose-Dateien. Sie dürfen nicht verwechselt werden.
|
||||
|
||||
`compose.yaml` ist ausschließlich für lokale Entwicklung vorgesehen. Es startet
|
||||
`tsx watch` und `next dev`, bindet Quellcode vom Host ein und enthält weder TLS,
|
||||
Authentifizierung, Reverse Proxy, Prozesshärtung noch ein zentral betriebenes
|
||||
Datenbanksystem. Der Stack darf deshalb nicht als produktionsreif bezeichnet oder
|
||||
öffentlich erreichbar gemacht werden.
|
||||
| Datei | Projektname | Zweck |
|
||||
| --- | --- | --- |
|
||||
| `compose.yaml` | `leistungsbilanz-dev` | ausschließlich lokale Entwicklung |
|
||||
| `compose.prod.yaml` | `leistungsbilanz` | Dauerbetrieb im LAN |
|
||||
|
||||
## Entwicklungs-Topologie
|
||||
`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.
|
||||
|
||||
| Komponente | Port | Healthcheck | Persistenz |
|
||||
| --- | ---: | --- | --- |
|
||||
| Next.js Web | 3001 | `GET /` | keine |
|
||||
| Express API | 3000 | `GET /health` | `./data:/app/data` |
|
||||
| SQLite | Datei | Integritäts-/FK-Prüfung via Backup und Skript | `data/leistungsbilanz.db` |
|
||||
`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:
|
||||
|
||||
|
|
@ -24,15 +43,66 @@ Verwendete Umgebungsvariablen:
|
|||
- `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` für lokale
|
||||
Dateibeobachtung in Docker
|
||||
- `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 zuerst `npm run db:migrate` und
|
||||
`npm run db:verify:circuit-schema`.
|
||||
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
|
||||
|
||||
|
|
@ -68,23 +138,40 @@ 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.
|
||||
|
||||
## Voraussetzungen für ein späteres Produktionssetup
|
||||
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`.
|
||||
|
||||
Vor einer produktiven Installation werden mindestens benötigt:
|
||||
## 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:
|
||||
|
||||
- reproduzierbare Produktionsimages und ein Next.js-Produktionsstartskript
|
||||
- TLS-Termination und Reverse Proxy
|
||||
- Authentifizierung, Autorisierung und Benutzer-/Rollenmodell
|
||||
- definierte Secrets- und Konfigurationsverwaltung
|
||||
- persistenter, gesicherter Datenbankbetrieb
|
||||
- kontrollierter einmaliger Migrationsschritt vor dem API-Rollout
|
||||
- Monitoring, strukturierte Logs und Alarmierung
|
||||
- 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
|
||||
|
||||
Bis diese Punkte umgesetzt und getestet sind, besteht die „Installation“ aus dem
|
||||
lokalen Entwicklungsstart in der README.
|
||||
Solange Authentifizierung fehlt, gehört der Stack ausschließlich in ein
|
||||
vertrauenswürdiges Netz oder hinter einen authentifizierenden Reverse Proxy.
|
||||
|
||||
## Backup und Wiederherstellung
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue