forked from jappel/leistungsbilanz-ts
Harden the container runtime and shut down cleanly
Running the stack on built output stopped the idle CPU load, but the remaining findings from the deployment report were still open. The Dockerfile is now multi-stage. The prod target runs on node:24-slim as the "node" user instead of root and carries only its runtime dependencies; "npm prune --omit=dev" drops tsx, drizzle-kit and typescript. The migration scripts need drizzle-orm and better-sqlite3, both regular dependencies, so they keep working. The dev target keeps devDependencies for compose.dev.yaml. Both compose files get a mem_limit. The server stack also sets memswap_limit to the same value, so a leak lets the OOM killer replace the container instead of pushing the host into swap. The Express API is no longer published on the host: it has no authentication and is reached through the Next.js rewrite. Development ports are bound to the loopback interface. Services drop all capabilities and run with no-new-privileges. SIGTERM and SIGINT now close the HTTP server and the SQLite handle and exit, instead of only logging the signal and waiting for the container's SIGKILL ten seconds later. server.closeIdleConnections() releases the keep-alive sockets the Next.js server holds, which would otherwise keep server.close() pending for the whole grace period. The compose start commands exec into node, because otherwise sh stays the foreground process and the signal never reaches it. Delete scripts/docker-start.sh rather than fixing its unsupervised background API: both compose stacks set their own command, so the single-container entrypoint has no caller left. Also remove drizzle.config.js (a stray tsc artifact drizzle-kit never reads) and the pre-Next index.html and styles.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
204499d7e4
commit
2e3e85a8e5
13 changed files with 238 additions and 121 deletions
65
compose.yaml
65
compose.yaml
|
|
@ -1,33 +1,59 @@
|
|||
# Server-Stack: gebauter Stand, keine Quellcode-Mounts, keine Datei-Watcher.
|
||||
# docker compose up --build --detach
|
||||
#
|
||||
# Fuer die Entwicklung mit Hot Reload compose.dev.yaml verwenden.
|
||||
name: leistungsbilanz
|
||||
|
||||
x-build: &build
|
||||
context: .
|
||||
target: prod
|
||||
args:
|
||||
# Baked into .next/routes-manifest.json by next build; see Dockerfile.
|
||||
# Wird von next build in .next/routes-manifest.json eingebacken, siehe
|
||||
# Dockerfile. Muss der API-Adresse in diesem Netz entsprechen.
|
||||
API_INTERNAL_URL: http://api:3000
|
||||
|
||||
x-logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "10"
|
||||
x-service-defaults: &service-defaults
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
# Der API-Prozess beendet HTTP-Server und SQLite-Handle auf SIGTERM; die
|
||||
# Frist gibt laufenden Requests Zeit, statt sie nach 10s in ein SIGKILL
|
||||
# laufen zu lassen.
|
||||
stop_grace_period: 20s
|
||||
# Das Image laeuft als "node" (UID 1000); zusaetzliche Rechte braucht
|
||||
# weder der API- noch der Web-Prozess.
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "10"
|
||||
|
||||
services:
|
||||
api:
|
||||
<<: *service-defaults
|
||||
build: *build
|
||||
# "exec" beim letzten Schritt: sonst bleibt sh der Vordergrundprozess und
|
||||
# SIGTERM erreicht node nie.
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- node scripts/run-migrations.js && node scripts/db-verify-circuit-schema.js && node dist/server/index.js
|
||||
- node scripts/run-migrations.js && node scripts/db-verify-circuit-schema.js && exec node dist/server/index.js
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: "3000"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
logging: *logging
|
||||
ports:
|
||||
- "3000:3000"
|
||||
# Nicht auf dem Host veroeffentlicht: die API hat keine Authentifizierung
|
||||
# und wird ueber den Next.js-Rewrite im Web-Dienst erreicht.
|
||||
expose:
|
||||
- "3000"
|
||||
# Harte Obergrenze statt unbegrenztem Wachstum. memswap_limit == mem_limit
|
||||
# schaltet Swap fuer den Container ab, damit ein Leck den Container trifft
|
||||
# und nicht den Host.
|
||||
mem_limit: 512m
|
||||
memswap_limit: 512m
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
healthcheck:
|
||||
|
|
@ -35,38 +61,43 @@ services:
|
|||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://localhost:3000/health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
- fetch('http://127.0.0.1:3000/health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
web:
|
||||
<<: *service-defaults
|
||||
build: *build
|
||||
command:
|
||||
- node_modules/.bin/next
|
||||
- start
|
||||
- -p
|
||||
- "3001"
|
||||
- --hostname
|
||||
- 0.0.0.0
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
API_INTERNAL_URL: http://api:3000
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
logging: *logging
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3001:3001"
|
||||
mem_limit: 1g
|
||||
memswap_limit: 1g
|
||||
healthcheck:
|
||||
# /web-health statt /health: next.config.mjs leitet /health an die API
|
||||
# weiter, und "/" wuerde wegen des Redirects auf /projects bei jeder
|
||||
# Probe zwei Seiten rendern.
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://localhost:3001/web-health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
- fetch('http://127.0.0.1:3001/web-health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue