wger: gunicorn with 2 workers instead of the development server (R-762)
gates / gates (push) Successful in 6s

WGER_USE_GUNICORN=True + WEB_CONCURRENCY=2. Proven on bench 9401 (anon 44 %) and
scratch 9202 (anon 64.1 % of 384M, 0 oom_kill, 0 restarts; login, CSS and a photo
200). wger stays lifecycle: hidden. No ladder entry: no image moves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-08 10:10:07 +02:00
parent 32d134639c
commit eec9a0d5af
4 changed files with 64 additions and 7 deletions
+23
View File
@@ -1,3 +1,26 @@
## 2026-10-08 (day) — wger runs on its real web server: gunicorn with 2 workers (R-762)
**What runs on a box changed:** nothing today. wger stays `lifecycle: hidden`, and no box runs it (hub read 2026-10-08
09:46 CEST: wger is in no box's deployed list, containers or app telemetry; paperless-ngx control present on 4 of 4).
- `templates/wger/docker-compose.yml`: `WGER_USE_GUNICORN=True` and `WEB_CONCURRENCY=2`, with a comment. Before this,
the image ran Django's development server (`manage.py runserver`). The image's entrypoint runs
`gunicorn ... --preload` only on that switch and passes no `-w`, so gunicorn reads its worker count from
`WEB_CONCURRENCY`. The header's RAM line now gives the measured figure.
- Proven on the bench (9401, harness v5 verdict `proven`, 606 s watch: anon peak 170 MiB = 44 % of 384M) and on scratch
9202 (a fresh install from the drill catalog through the product's deploy endpoint): 2 × „Booting worker" in the
log; login page 200; its three CSS links 200 through `wger-files`; a photo uploaded 201 and read back 200 (179 B,
`image/png`); an unknown `/static/` file 404 (control). 9202's 600 s watch (10,944 requests): anon peak 246 MiB =
64.1 % of 384M, flat for 1,200 more page loads after; oom_kill 0, restarts 0.
- **No ladder entry:** the change moves no image (from == to). `ladder.check_entry` refuses a re-test with the same
digest („tests nothing new"), and `09` §5.4 renders the catalog template when the images equal the pinned ones. So the
change reaches an installed wger at its next `up -d`.
- `onboarding/wger.md`: row 1.5 done (gunicorn); row 1.7's „stays unset" sentence corrected; row 9.3's open-row count
(8) and register rows corrected.
- Evidence: `felhom.eu/documentation/audits/day-2026-10-08/r762/` (`bench/`, `9202/`). Gates: `catalog_gates.py --fast`
green. The runtime gates (image-resolvable, volume-persistence) were not run: they need a container runtime, and
DooPlex gets no Docker command.
## Unreleased (2026-10-07) — the shared rule file; no code change
- `.claude/rules/unprompted-work.md`: rule 11 (every helper prompt carries the brief's fences in full, `09` §3 decision 160) and the §1 line „no hub image build or hub deploy in a session the operator does not attend" (decision 162). Identical in all five copies.
+29 -3
View File
@@ -1,4 +1,30 @@
# REPORT — no code change (2026-10-07 day)
# REPORT — wger runs on gunicorn with 2 workers (R-762, 2026-10-08 day)
Only the shared rule file changed (`09` §3 decision 162: no hub image build or deploy in a session the operator does not
attend; and rule 11, every helper prompt carries the brief's fences in full). Controller 0.302.0 stays on the three boxes.
**Change:** `templates/wger/docker-compose.yml` sets `WGER_USE_GUNICORN=True` and `WEB_CONCURRENCY=2`. Before this, wger
ran Django's development server. wger stays `lifecycle: hidden`. No box runs wger (hub read 2026-10-08 09:46 CEST).
| venue | workers | anon peak (of 384M) | oom_kill | restarts | login | CSS (3 links) | photo read back |
|---|---|---|---|---|---|---|---|
| bench 9401 (harness v5, 606 s) | 2 | 170 MiB, 44 % | 0 | 0 | 200 | 200 | 200 |
| scratch 9202 (drill catalog, product deploy, 600 s) | 2 | 246 MiB, 64.1 % | 0 | 0 | 200 | 200 | 200 (179 B, image/png) |
On 9202 the anon figure stayed at 246 MiB for 1,200 more page loads after the watch. Control: an unknown `/static/` file
answered 404.
**No ladder entry.** The images do not move (from == to). `ladder.check_entry` refuses a same-digest re-test, and
`09` §5.4 renders the catalog template when the images equal the pinned ones.
**Also corrected:** `onboarding/wger.md` rows 1.5, 1.7 and 9.3.
**Not changed:** `mem_request: "100M"` in `.felhom.yml`. The measured figure is about 250 MiB, but the controller uses
`mem_request` in its install memory check (`deploy.go` L305, `update.go` L489). A change there changes which boxes can
install wger, so it is for the operator.
**Gates:** `catalog_gates.py --fast` green. The runtime gates were not run, because DooPlex gets no Docker command.
**Teardown on 9202:** wger was removed through the product. Afterwards there was no container and no volume, and the
stack directory held only the synced catalog template, as before the test. 9202's catalog setting was put back
(`repo_url` = the live catalog; `controller.yaml` sha256 identical to before). The drill commit was reverted (drill
`95876a3`).
Evidence: `felhom.eu/documentation/audits/day-2026-10-08/r762/`.
+3 -3
View File
@@ -26,9 +26,9 @@ Measured on 9202 2026-10-01 from the drill catalog (e9f50b5, wger template ident
1.2 | n/a | wger keeps SQLite on its own volume, no database sidecar
1.3 | n/a | no MariaDB or PostgreSQL service in this template
1.4 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C2-static-reads.txt ; felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/wger/step.txt — `DJANGO_PERFORM_MIGRATIONS=True`; the 2.6 -> 2.7 step migrated and read back on the box
1.5 | open | the container runs `manage.py runserver` — `WGER_USE_GUNICORN` is not set (R-755; C2)
1.5 | done | gunicorn with 2 workers since 2026-10-08 (`WGER_USE_GUNICORN=True`, `WEB_CONCURRENCY=2`, R-762): 2 × „Booting worker" in the log; login page, CSS and a photo 200; 10-min watch 0 oom_kill, 0 restarts — bench 9401 and 9202 (`felhom.eu/documentation/audits/day-2026-10-08/r762/`)
1.6 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C2-static-reads.txt ; felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C3-logins.txt — of 15 key-like env reads: SECRET_KEY generated, JWT pair made at start (phone route 200), DB password unused by SQLite; the rest belong to features off here (S3, mail, reCAPTCHA, OIDC, PowerSync)
1.7 | done | `DJANGO_DEBUG=False` (R-762, 2026-10-06): `collectstatic` runs at start; the static files are served by `wger-files` (nginx) — the login page's own CSS links 200 on the bench and on 9202 (`felhom.eu/documentation/audits/design-build-2026-10-06/F/`). `WGER_USE_GUNICORN` stays unset (R-755, closed)
1.7 | done | `DJANGO_DEBUG=False` (R-762, 2026-10-06): `collectstatic` runs at start; the static files are served by `wger-files` (nginx) — the login page's own CSS links 200 on the bench and on 9202 (`felhom.eu/documentation/audits/design-build-2026-10-06/F/`). the web server is gunicorn since 2026-10-08 (row 1.5)
1.8 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C2-static-reads.txt ; felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C3-logins.txt — DEBUG False; an unknown page is a plain 404, no debug page
1.9 | n/a | SECRET_KEY signs sessions and reset links only; the JWT pair lives on the data volume and is backed up with it
2.1 | open | re-sweep 2026-10-02 with the fixed gate (felhom.eu/documentation/audits/persistence-sweep-2026-10-02/A/sweep/TABLE.md): UNDETERMINED — /home/wger/media stays empty after the seed (no upload in the seed); nothing written outside a preserved folder (R-807). The August CLEAN came from start-time writes alone
@@ -71,5 +71,5 @@ Measured on 9202 2026-10-01 from the drill catalog (e9f50b5, wger template ident
8.5 | n/a | wger is an existing app; the count does not change
9.1 | open | the runtime volume-persistence gate was not re-run today (last CLEAN 2026-08-02); the other gates exit 0 (B3) (R-759)
9.2 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C1-install.txt ; felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt — fresh installs from the drill catalog, as household and stranger
9.3 | open | 10 other rows stay open; each is a register row (R-755, R-759, R-762, R-763, R-764)
9.3 | open | 8 other rows stay open; each is a register row (R-759, R-763, R-764, R-807)
9.4 | n/a | wger is exempt: published 2026-02-15, before the checklist
+9 -1
View File
@@ -1,7 +1,7 @@
# wger - Edzésnapló és fitnesz tervező
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: None (file-based)
# RAM: ~100M (mem_limit: 384M) | Pi-compatible: Yes
# RAM: ~250M with 2 gunicorn workers (measured 2026-10-08; mem_limit: 384M + wger-files 32M) | Pi-compatible: Yes
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
@@ -83,6 +83,14 @@ services:
# /home/wger/static, and Django stops serving /static and /media itself (it never did in production — upstream
# puts nginx in front). wger-files below serves both from the shared volumes.
- DJANGO_DEBUG=False
# R-762 (2026-10-08): the real web server. The image's entrypoint.sh runs `gunicorn wger.wsgi:application --preload
# --bind 0.0.0.0:$PORT` when WGER_USE_GUNICORN is "True" (else Django's development server, `manage.py runserver`).
# It passes no -w and the image has no gunicorn.conf.py, so the worker count is gunicorn's own WEB_CONCURRENCY
# (unset = 1). Two workers: measured on the bench and on 9202 (2 x "Booting worker" in the log). anon peak under
# load: 170 MiB (44 %) on the bench, 246 MiB (64 %) on 9202 after a photo upload, flat for 1,200 more page loads;
# 0 oom_kill, 0 restarts. --preload: the workers share the app's pages with the master.
- WGER_USE_GUNICORN=True
- WEB_CONCURRENCY=2
volumes:
- wger_data:/home/wger/db
- wger_media:/home/wger/media