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

30
scripts/docker-start.sh Normal file → Executable file
View file

@ -1,4 +1,7 @@
#!/bin/sh
# Legacy single-container entrypoint. Kept for the existing deployment that
# still runs both processes in one container; new deployments use
# compose.prod.yaml, which runs the API and the web server separately.
set -e
echo "Running migrations..."
@ -6,11 +9,34 @@ node scripts/run-migrations.js
echo "Starting API server on :3000..."
node dist/server/index.js &
api_pid=$!
echo "Waiting for API..."
until node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" 2>/dev/null; do
until node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" 2>/dev/null; do
if ! kill -0 "$api_pid" 2>/dev/null; then
echo "API process exited during startup."
exit 1
fi
sleep 1
done
echo "Starting Next.js on :3001..."
exec node_modules/.bin/next start -p 3001
node_modules/.bin/next start -p 3001 &
web_pid=$!
terminate() {
kill -TERM "$api_pid" "$web_pid" 2>/dev/null || true
}
trap terminate TERM INT
# Previously the API ran unsupervised in the background: when it died the
# container stayed "up" and served a frontend whose every request failed.
# Exiting here lets the restart policy replace the container instead.
while kill -0 "$api_pid" 2>/dev/null && kill -0 "$web_pid" 2>/dev/null; do
sleep 5
done
echo "A server process exited; stopping the container."
terminate
wait "$api_pid" "$web_pid" 2>/dev/null || true
exit 1