forked from jappel/leistungsbilanz-ts
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>
102 lines
2.8 KiB
YAML
Executable file
102 lines
2.8 KiB
YAML
Executable file
# Production deployment.
|
|
#
|
|
# Differences to compose.yaml that matter operationally:
|
|
# - runs compiled output (node dist/, next start) instead of watch mode
|
|
# - no source bind mounts and no file watchers, so an idle stack costs ~0% CPU
|
|
# - healthchecks hit the cheap /health JSON endpoint, not a rendered page
|
|
# - every service has a hard memory limit and may not spill into host swap
|
|
# - the Express API is reachable only inside the compose network
|
|
#
|
|
# Usage:
|
|
# docker compose -f compose.prod.yaml up --build --detach
|
|
name: leistungsbilanz
|
|
|
|
x-service-defaults: &service-defaults
|
|
init: true
|
|
restart: unless-stopped
|
|
stop_grace_period: 20s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "20m"
|
|
max-file: "10"
|
|
|
|
services:
|
|
api:
|
|
<<: *service-defaults
|
|
build:
|
|
context: .
|
|
target: prod
|
|
args:
|
|
# Baked into the Next.js rewrite manifest; see the build stage in the
|
|
# Dockerfile. Must match the API service address in this network.
|
|
API_INTERNAL_URL: http://api:3000
|
|
command:
|
|
- sh
|
|
- -c
|
|
- node scripts/run-migrations.js && exec node dist/server/index.js
|
|
environment:
|
|
NODE_ENV: production
|
|
PORT: "3000"
|
|
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
|
# Not published on the host: the API has no authentication and is reached
|
|
# through the Next.js rewrite in the web service.
|
|
expose:
|
|
- "3000"
|
|
mem_limit: 512m
|
|
memswap_limit: 512m
|
|
volumes:
|
|
- leistungsbilanz-data:/app/data
|
|
healthcheck:
|
|
test:
|
|
- CMD
|
|
- node
|
|
- -e
|
|
- fetch('http://127.0.0.1:3000/health').then(response=>process.exit(response.ok?0:1)).catch(()=>process.exit(1))
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
web:
|
|
<<: *service-defaults
|
|
build:
|
|
context: .
|
|
target: prod
|
|
args:
|
|
# Baked into the Next.js rewrite manifest; see the build stage in the
|
|
# Dockerfile. Must match the API service address in this network.
|
|
API_INTERNAL_URL: http://api:3000
|
|
command:
|
|
- node_modules/.bin/next
|
|
- start
|
|
- -p
|
|
- "3001"
|
|
- --hostname
|
|
- 0.0.0.0
|
|
environment:
|
|
NODE_ENV: production
|
|
API_INTERNAL_URL: http://api:3000
|
|
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
ports:
|
|
- "3090:3001"
|
|
mem_limit: 1g
|
|
memswap_limit: 1g
|
|
healthcheck:
|
|
# /health is rewritten to the API, so this verifies the web process and
|
|
# its API connectivity for the cost of one JSON response.
|
|
test:
|
|
- CMD
|
|
- node
|
|
- -e
|
|
- fetch('http://127.0.0.1:3001/health').then(response=>process.exit(response.ok?0:1)).catch(()=>process.exit(1))
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
volumes:
|
|
leistungsbilanz-data:
|