diff --git a/.env.example b/.env.example index 3a76bc3..5653c24 100644 --- a/.env.example +++ b/.env.example @@ -427,3 +427,14 @@ VISION_MANAGER_DATABASE_URL= # VISION_MEM_LOW_WATERMARK_PCT used% >= this → inhibit starts (keep HIGH - LOW >= ~10 so the band beats jitter). Default 80. # VISION_MEM_YIELD_COOLDOWN Seconds after a pressure-stop during which restart is refused regardless of backlog. Default 300. # VISION_MEM_POLL_INTERVAL Mem sub-poll cadence, seconds (<= VISION_POLL_INTERVAL); catches spikes between backlog ticks. Default 5. + +# ---- Deployment resource bounds & exposure (docker-compose.yml) ---- +# Host interface the frontend port publishes on. Default 127.0.0.1 so SvelteKit +# (plain HTTP) is reachable only by a host-local reverse proxy. Set 0.0.0.0 only +# if your TLS terminator runs on a different host. +FRONTEND_PUBLISH_ADDR=127.0.0.1 +# Per-container memory ceilings (docker mem_limit). Generous defaults; tune to +# your host. The backend runs OCR + headless Chromium and is the heavy one. +BACKEND_MEM_LIMIT=4g +FRONTEND_MEM_LIMIT=512m +POSTGRES_MEM_LIMIT=1g diff --git a/backend/src/config.rs b/backend/src/config.rs index 26c778d..86916a7 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -1096,6 +1096,13 @@ mod tests { "BACKEND_URL", "BACKEND_PROXY_TIMEOUT_MS", "VISION_MANAGER_DATABASE_URL", + // Compose-level deployment knobs (port publish interface + per-container + // memory ceilings) — consumed by docker-compose.yml itself, never read + // by the backend process, so they don't belong in its environment block. + "FRONTEND_PUBLISH_ADDR", + "BACKEND_MEM_LIMIT", + "FRONTEND_MEM_LIMIT", + "POSTGRES_MEM_LIMIT", ]; /// Keys whose compose RHS is intentionally NOT a `${KEY...}` diff --git a/docker-compose.yml b/docker-compose.yml index 4f08c3a..f013679 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,12 @@ services: # takes the whole stack offline. Match the policy already set on # tor / docker-socket-proxy / vision-manager. restart: unless-stopped + # Bound Postgres so a runaway query/backlog can't consume all host RAM. + # Tune to your host via .env (should comfortably exceed shared_buffers + + # work_mem * max_connections). + mem_limit: ${POSTGRES_MEM_LIMIT:-1g} + security_opt: + - no-new-privileges:true tor: # SOCKS5 proxy for the crawler, plus a control port so the backend @@ -197,6 +203,13 @@ services: expose: - "8080" restart: unless-stopped + # Bound the heaviest consumer: the backend hosts the OCR/analysis worker and + # drives a headless Chromium crawler, either of which can balloon and + # OOM-kill the host (taking Postgres with it) with no limit. Tune to your + # host via .env — this default is a generous ceiling, not a target. + mem_limit: ${BACKEND_MEM_LIMIT:-4g} + security_opt: + - no-new-privileges:true frontend: build: ./frontend @@ -213,8 +226,17 @@ services: # to 300000 (5 min) in hooks.server.ts; raise/lower via .env. BACKEND_PROXY_TIMEOUT_MS: ${BACKEND_PROXY_TIMEOUT_MS:-300000} ports: - - "3000:3000" + # Bind to loopback by default so SvelteKit (plain HTTP; COOKIE_SECURE=true + # expects TLS in front) is reachable ONLY by a host-local reverse proxy, + # not cleartext over the LAN/internet. A host-external TLS terminator can + # set FRONTEND_PUBLISH_ADDR=0.0.0.0 (or a specific interface) in .env. + - "${FRONTEND_PUBLISH_ADDR:-127.0.0.1}:3000:3000" restart: unless-stopped + # Cap runaway memory (SvelteKit is light, but bound it anyway) and forbid + # privilege escalation. Tune the limit to your host via .env. + mem_limit: ${FRONTEND_MEM_LIMIT:-512m} + security_opt: + - no-new-privileges:true # ----- Vision autoscaling (profile: ai) ----------------------------------- # Two extra containers that idle-stop the mangalord-vision (llama.cpp)