Tune gunicorn/Postgres/Redis for a memory-limited server

- gunicorn: 3 workers -> 2 (this workload isn't CPU-bound per
  DEPLOYMENT.md's own sizing), add --preload so workers share
  immutable memory via copy-on-write instead of each independently
  importing Django, add --max-requests so a worker that renders a
  WeasyPrint invoice doesn't carry that memory forever.
- Postgres: trim shared_buffers/max_connections from the image
  defaults (128MB/100), sized for a ~0.2GB dataset instead.
- Redis: cap with --maxmemory as a ceiling, not a saving.
This commit is contained in:
2026-08-06 22:29:20 +02:00
parent 1be9959481
commit fe19a6f08a
4 changed files with 31 additions and 5 deletions

View File

@@ -59,6 +59,11 @@ services:
POSTGRES_DB: ${POSTGRES_DB:-rosterchief}
POSTGRES_USER: ${POSTGRES_USER:-rosterchief}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set a database password}
# shared_buffers/max_connections default to 128MB / 100 — sized for a much bigger database
# than this app's (DEPLOYMENT.md: ~0.2GB after 5 years). 20 connections is comfortably above
# 2 gunicorn workers x 4 threads plus the odd `manage.py` one-off; trimmed both for the box,
# not for the data.
command: ["postgres", "-c", "shared_buffers=64MB", "-c", "max_connections=20"]
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
@@ -70,9 +75,11 @@ services:
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
# Cache only, so nothing here needs to survive a restart. It is not optional though: it
# is what keeps every gunicorn worker agreeing about which feature flags are on.
# is what keeps every gunicorn worker agreeing about which feature flags are on. maxmemory
# is a ceiling, not a saving — this is already the smallest process in the stack — but on a
# memory-limited box it should evict cache entries under pressure, not grow unbounded.
command: ["redis-server", "--save", "", "--appendonly", "no", "--maxmemory", "32mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
pgdata: