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

@@ -423,7 +423,8 @@ So do not size for the data. Size for the **processes**.
### What actually consumes the box
Measured, running this app under gunicorn with `DEBUG=False`:
Measured, running this app under gunicorn with `DEBUG=False`, before the tuning below —
`--workers 3`, no `--preload`, Postgres and Redis on their image defaults:
| | memory |
|---|---|
@@ -434,6 +435,13 @@ Measured, running this app under gunicorn with `DEBUG=False`:
| OS + Docker daemon | ~400 MB |
| **steady state** | **~1.01.2 GB** |
Since then, `Dockerfile`/`compose.yaml` were tuned for smaller boxes: `--workers 2 --preload`
(one fewer duplicated Django process, and `--preload` shares immutable memory across workers
via copy-on-write instead of each worker importing Django independently), plus trimmed Postgres
`shared_buffers`/`max_connections` and a Redis `--maxmemory` cap. Expect the gunicorn and
Postgres rows to come in lower than above — not yet re-measured, so treat the table as the
shape of where memory goes rather than exact numbers on the current config.
2 GB would run it. 4 GB is the recommendation for three reasons, all of which are the kind of
thing that bites at the worst moment: