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:
Grovy311 2026-08-15 00:26:56 +02:00
parent a17e2e3f4b
commit 01fa527b9c
14 changed files with 424 additions and 118 deletions

33
AGENTS.md Normal file → Executable file
View file

@ -531,13 +531,44 @@ Users must be able to override sizing suggestions.
- Preserve stable UUIDs and explicit transaction boundaries for a later
PostgreSQL adapter.
## Deployment Rules
There are two compose files and they must stay separate.
- `compose.yaml` is development only. Project name `leistungsbilanz-dev`, ports
bound to `127.0.0.1`, watch-mode servers, source bind mounts.
- `compose.prod.yaml` is the deployment. Project name `leistungsbilanz`, image
target `prod`, compiled output, no bind mounts, no file watchers.
Rules that a running deployment depends on:
- Never run the development stack permanently. `next dev` and `tsx watch`
produce load and grow in memory with no user activity.
- Never reintroduce `CHOKIDAR_USEPOLLING` or `WATCHPACK_POLLING`. They make the
watcher poll the source tree continuously.
- Container healthchecks target `/health` only. `/` is a redirect to
`/projects`, and `fetch` follows redirects, so probing `/` renders a full
page on every interval.
- Every service keeps a `mem_limit`; production services also keep a
`memswap_limit` of the same size so a leak kills the container instead of the
host.
- The production image runs as `node`. Data volumes written by an earlier root
container need a one-time `chown` (see `docs/deployment.md`).
- The Express API is not published on the host. It has no authentication and is
reached through the Next.js rewrite.
- Development migrates with `drizzle-kit`; the production image has no
devDependencies and migrates with `node scripts/run-migrations.js`.
- `SIGTERM`/`SIGINT` must close the HTTP server and the SQLite handle and then
exit. Do not reduce these handlers to logging.
## Current Deferred Work
- Revit/CSV/IFCGUID round-trip, except when Phase 14 is explicitly requested and
`docs/spec/revit-csv-integration-requirements.md` is being followed
- full electrical sizing
- multi-user/PostgreSQL operation
- supported production deployment
- authentication, authorization and TLS termination; until they exist the
deployment belongs in a trusted network or behind an authenticating proxy
Do not implement these while working on an unrelated phase.