fix: gunicorn control socket error and host bytecode in the image

Found by running the container on Docker Desktop. Disable gunicorn's control
socket (the home directory is read-only) and ignore __pycache__ and .pyc at
any depth so host bytecode with the Windows source path stays out of the image.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sk72AxLwQnYttF6VFSxQLt
This commit is contained in:
Ebbe Baß
2026-10-15 14:58:32 +02:00
co-authored by Claude Sonnet 5.5
parent f8b0584687
commit 840bea1cba
3 changed files with 9 additions and 4 deletions
@@ -80,7 +80,9 @@ Greenfield: only planning documents exist. New files, using the spine's source t
- `README.md` was rewritten and no longer carries the original project name line.
- Ruff excludes `_bmad`, `_bmad-output`, `.agents` and `context-resources`.
- The 503 criterion is tested with a mocked unreachable database, not an unreadable file.
- Not verified: the Docker build, the read-only root file system with gunicorn's worker temp dir, the `uv` image tag and the container health check. They need `docker compose up --build` on the Linux host.
- Docker was verified on the development machine afterwards (Docker Desktop 29.8.2, Linux engine): the image builds, the container is healthy, runs as uid 10001, has a read-only root file system with `/data`, `/backups` and `/tmp` writable, answers `/healthz` with 200 and all security headers, returns 400 with the CSP for an unlisted Host, keeps the database across a restart, exits 1 on invalid configuration naming every bad variable, and logs no query strings.
- The live test found two defects the unit tests could not: gunicorn 26 logged a startup error because its control socket could not be created in the read-only home directory (fixed with `--no-control-socket`), and host `__pycache__` files, which embed the Windows source path, were copied into the image because `.dockerignore` only matched the top-level folder (fixed with `**/__pycache__` and `**/*.pyc`). The image no longer contains the project name.
- Still to confirm on the Linux host: the same checks behind Zoraxy, and that the uv image tag builds there.
- `compose.yaml` publishes port 8000 on all host interfaces. Bind it to a specific address if the reverse proxy runs on the same host.
- Resolved versions in `uv.lock`: Django 5.2.18, gunicorn 26.2.0, whitenoise 6.12.0.
@@ -100,6 +102,8 @@ Pass 1 (quick lens). Counts: high 1, medium 3, low 3, false 1, maybe-false 0.
| 6 | Gunicorn error log is plain text on stderr | low | rejected | Server start and error lines only; app logs are JSON. A JSON gunicorn log config is more than a direct correction. |
| 7 | "No ping-mee name" not satisfiable because of the plan file | false | rejected | The criterion covers code and image; `.dockerignore` excludes `_bmad-output` and a search outside protected folders found nothing. |
| 8 | 503 criterion tested with a mock, not an unreadable file | low | rejected | Disclosed in Implementation Notes; the view returns 503 on any exception from `cursor()`, so a real failure cannot return 200. A real-file test needs database wrapper plumbing. |
| 9 | Gunicorn startup error: control socket cannot be created (found in the live container test) | medium | patch | `[ERROR] Control server error: [Errno 30] Read-only file system: '/home/app'` at every start; fixed with `--no-control-socket`, error gone. |
| 10 | Host `__pycache__` copied into the image, embedding the Windows source path (found in the live container test) | medium | patch | `grep` in the image found the project name in `.pyc` files; `.dockerignore` matched only the top-level folder; fixed with `**/` patterns, rescan is clean. |
## Design Notes