Pass the whole .env to the container, and refuse the default session secret (#147) #160

Merged
claude-bot merged 2 commits from fix/147-compose-env-file into main 2026-09-26 05:32:34 +00:00
Collaborator

Fixes #147.

What was wrong

docker-compose.yml passed config through a hand-maintained environment: list that stopped growing after v1.0.0. The app's own .env loading resolves to /app/.env inside the container, which is never mounted. So 19 settings in .env.example silently did nothing under docker compose up, and ten of them are env-only, which made them impossible to turn on:

  • METRICS_ENABLED
  • BACKUP_REMOTE_PATH
  • the six BACKUP_S3_* settings
  • AURORA_ENABLED and AURORA_MIN_KP

Changes

  • docker-compose.yml: env_file: .env replaces the list, so there is no second copy to drift.
  • SESSION_SECRET guard moves into the app (app/config.py assert_session_secret_is_safe, called at import in app/main.py).
    • The compose :? was the only real guard; the app only logged CRITICAL and started anyway.
    • The app now refuses to start with the default secret while auth is on.
    • An empty env value falls back to the default via env_ignore_empty, so it is caught too.
    • startup.sh's uvicorn is the only non-test importer of app.main. Tests are unaffected because conftest.py sets DISABLE_AUTH=true before import.
  • Tests:
    • test_env_example.py: every .env.example name must be a Settings field. The allowlist is HOST_PORT, IRIS_UID and IRIS_GID, which are compose-interpolation only.
    • test_config_guard.py: four cases, including the empty-env-var path.
  • CI: a step in docker-e2e renders docker compose config against a sentinel .env and asserts five values reach .services.iris.environment. This matters because the E2E container is started with docker run, so nothing else crosses this boundary. It was verified locally in both directions: it passes with the fix and fails with a reverted list.
  • Docs: the spec §13.1 compose example is corrected, the ASVS review note is updated, and there is a CHANGELOG entry with an upgrade note.

Verification

  • Local: 852 backend tests pass, and ruff lint and format are clean.
  • mypy could not run on the Windows dev box (blocked by an Application Control policy), so this PR's CI is the first mypy run on the change.

Behaviour change to note

A missing or default SESSION_SECRET now shows up as a container that exits at startup, with the fix in its log, instead of docker compose up refusing to run. TZ loses its compose-level UTC default, but Settings.TZ also defaults to UTC, so nothing changes there.

Next: release as v1.1.1, so Rhoving/iac-repo#527 can pin a tag with this fix.

🤖 Generated with Claude Code

Fixes #147. ## What was wrong `docker-compose.yml` passed config through a hand-maintained `environment:` list that stopped growing after v1.0.0. The app's own `.env` loading resolves to `/app/.env` inside the container, which is never mounted. So 19 settings in `.env.example` silently did nothing under `docker compose up`, and ten of them are env-only, which made them impossible to turn on: - `METRICS_ENABLED` - `BACKUP_REMOTE_PATH` - the six `BACKUP_S3_*` settings - `AURORA_ENABLED` and `AURORA_MIN_KP` ## Changes - **`docker-compose.yml`:** `env_file: .env` replaces the list, so there is no second copy to drift. - **`SESSION_SECRET` guard moves into the app** (`app/config.py` `assert_session_secret_is_safe`, called at import in `app/main.py`). - The compose `:?` was the only real guard; the app only logged CRITICAL and started anyway. - The app now refuses to start with the default secret while auth is on. - An empty env value falls back to the default via `env_ignore_empty`, so it is caught too. - `startup.sh`'s uvicorn is the only non-test importer of `app.main`. Tests are unaffected because `conftest.py` sets `DISABLE_AUTH=true` before import. - **Tests:** - `test_env_example.py`: every `.env.example` name must be a `Settings` field. The allowlist is `HOST_PORT`, `IRIS_UID` and `IRIS_GID`, which are compose-interpolation only. - `test_config_guard.py`: four cases, including the empty-env-var path. - **CI:** a step in `docker-e2e` renders `docker compose config` against a sentinel `.env` and asserts five values reach `.services.iris.environment`. This matters because the E2E container is started with `docker run`, so nothing else crosses this boundary. It was verified locally in both directions: it passes with the fix and fails with a reverted list. - **Docs:** the spec §13.1 compose example is corrected, the ASVS review note is updated, and there is a CHANGELOG entry with an upgrade note. ## Verification - Local: 852 backend tests pass, and ruff lint and format are clean. - **mypy could not run on the Windows dev box** (blocked by an Application Control policy), so this PR's CI is the first mypy run on the change. ## Behaviour change to note A missing or default `SESSION_SECRET` now shows up as a container that exits at startup, with the fix in its log, instead of `docker compose up` refusing to run. `TZ` loses its compose-level `UTC` default, but `Settings.TZ` also defaults to `UTC`, so nothing changes there. Next: release as v1.1.1, so Rhoving/iac-repo#527 can pin a tag with this fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docker-compose.yml maintained its own copy of every setting in
.env.example as an `environment:` list, and nothing kept the two in
sync: 19 settings added since -- METRICS_ENABLED, the BACKUP_S3_*
family, AURORA_ENABLED/AURORA_MIN_KP, RAMP_ON/OFF_MINUTES,
DAILY_BACKUP_RETAIN_COUNT, and the morning-window block -- were never
added to compose, so setting them in .env silently did nothing to a
container started with `docker compose up`. Switch to `env_file: .env`
so there is no second list to drift, and add a CI step that starts a
sentinel .env through `docker compose config` and asserts it reaches
the container's environment.

The same drift had removed the only thing stopping Iris from starting
with the insecure default SESSION_SECRET: compose's `${SESSION_SECRET:?...}`
was the sole guard, and it only worked when running the image through
this exact compose file. The app now refuses to start with the default
secret while authentication is enabled instead of only logging a
CRITICAL line, so the guard holds regardless of how the container is
run. Add tests for both the .env.example/Settings agreement and the
new startup guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Say what #147 changes for someone upgrading, and correct what the CI step does
All checks were successful
CI / Pre-commit hooks (pull_request) Successful in 54s
CI / Python lint & type-check (pull_request) Successful in 2m14s
CI / Dockerfile lint (pull_request) Successful in 8s
CI / Alembic migration check (pull_request) Successful in 1m17s
CI / Frontend lint, test & build (pull_request) Successful in 1m41s
CI / Python tests (pull_request) Successful in 4m39s
CI / Docker build, health smoke & E2E (pull_request) Successful in 2m7s
a6a61aa6a6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch fix/147-compose-env-file 2026-09-26 05:32:35 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED!160
No description provided.