uvicorn --forwarded-allow-ips=* — leftmost XFF for its pair-code limits. Once traefik trusts the tunnel's fixed address (controller v0.286.0), the leftmost entry is what a
stranger writes; with the chain removed the app reads traefik's X-Real-Ip or its peer, as before — never forgeable.
Measured on 9202: a router with this middleware receives no X-Forwarded-For (felhom.eu audits/visitors-2026-10-01/A/P1).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
WEB_SERVER_CONCURRENCY=2, limit left at 768M. No image: line moved.
768M was still a guess and it was wrong: kills slowed from ~12/min to ~7/min and stopped nothing.
Measured instead - each warm uvicorn worker holds ~216 MiB, so four plus the master reach ~882 MiB,
and the cgroup's memory.peak read exactly 768 MiB.
/init:143 runs --workers "${WEB_SERVER_CONCURRENCY:-4}". Four is a server default; this is one
household on one small box. Two measure ~450 MiB, fit 768M with headroom, and halve the CPU churn.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
No image: line moved, so no catalog_since moved.
Measured on demo-hp after this morning's promotion: OOMKilled true, 4530 gunicorn worker SIGKILLs
in six hours, ~500% CPU in a permanent restart storm, host load 5.2 while otherwise idle. It ran
clean for two hours first, which is why the walk on the scratch guest did not catch it.
The update reported `done` and the app read `running` the whole time - nginx answers 200 while the
workers behind it die. Nothing alarmed; the operator heard the fans.
768M is a measured first step, not a final answer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Walked through the product's own guarded Update on scratch guest 9202 during the
update night, seeded and read back through the app's own front door.
Evidence: felhom.eu/documentation/audits/update-night-2026-09-21/apps/romm/verdict.json
catalog_since -> 2026-09-22 (R-452).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
bookstack-db, kimai-db, nextcloud-db, romm-db each gain `MARIADB_AUTO_UPGRADE=1` in the db
service's environment list. Operator ruling 2026-09-13 on the measurement in
felhom.eu/documentation/audits/SPIKE-r459-mariadb-upgrade-2026-09-06.md: an unconverted datadir
is stable but never heals; the conversion costs ~7 s and the engine backs its system tables up
first. MARIADB_DISABLE_UPGRADE_BACKUP is deliberately left UNSET — that backup is the precaution.
NO `image:` line changed, so `catalog_since` does NOT move — the CLAUDE.md rule ties it to an
image change and this is not one. Do not "fix" that.
The setting is inert until an engine major actually moves, and none may until Slice 4 (R-448)
ships — see the engine-major rule in CLAUDE.md and scripts/check-engine-major.py (next commit).
The eleven PostgreSQL templates are untouched: R-463 is a different engine and a different
measurement.
REUSE.md: one convention row for the MariaDB sidecar env, same commit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
BusyBox wget (+ node/python/curl one-shots, incl mealie's socket tuple) resolve
localhost -> IPv6 ::1 with no cross-family fallback; an IPv4-only-binding app
reads docker-unhealthy while serving (vaultwarden, re-run 2026-07-06). Escalates
that instance to the class. Scoped strictly to healthcheck test: lines
(diff-reviewed: no env/config/label changed; .felhom.yml already clean). New
REUSE.md convention row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Model A binds the guest mount /mnt/<drive> directly onto the host's
<drive>/felhom-data namespace, so the guest mount already IS felhom-data.
The templates' ${HDD_PATH}/felhom-data/appdata/<app> therefore double-nested
to <drive>/felhom-data/felhom-data/appdata/<app> on disk, diverging from the
provenance-aware backup helpers (NamespaceRoot(drive,true) -> single-nested).
Change all four HDD app templates (romm, nextcloud, immich, paperless-ngx)
to ${HDD_PATH}/appdata/<app>, matching AppDataDir(NamespaceRoot(HDD_PATH,true)).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compose templates were mounting app data at ${HDD_PATH}/appdata/ instead
of ${HDD_PATH}/felhom-data/appdata/ as designed in the v0.26.0+ path
structure. Affects: nextcloud, immich, paperless-ngx, romm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 51 docker-compose.yml: replaced hardcoded subdomain.${DOMAIN}
with ${SUBDOMAIN}.${DOMAIN} in Traefik labels, app env vars, and
comments.
All 51 .felhom.yml: added SUBDOMAIN deploy field (type: subdomain)
with default matching existing subdomain metadata value.
Works with felhom-controller v0.27.0 which validates and stores the
user-chosen subdomain in app.yaml. Existing deployed apps get
SUBDOMAIN auto-injected via InjectMissingFields() on next sync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Part of v0.14.0 storage architecture overhaul — standardize
app data paths under appdata/ instead of storage/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>