An upgrade repairs a root-owned media volume by itself (#1064) #1068

Merged
claude-bot merged 1 commit from fix/1064-media-volume-ownership into main 2026-10-04 04:20:12 +00:00
Collaborator

Closes #1064.

Summary

#914 made the image create /app/media owned by appuser, which only helps a volume Docker initialises from that image. A media volume that already holds files and is root-owned stays that way, and the backend cannot write uploads into it.

The migrate service already runs as root before the app starts, to prepare ./backups (#414). It now also mounts media and repairs it:

if [ "$(stat -c %u /app/media)" != "$(id -u appuser)" ]; then
  echo 'migrate: /app/media is not owned by appuser; repairing (#1064)'
  chown -R appuser:appuser /app/media
fi
  • Cost. One stat of the top-level owner on every start. The recursive chown runs only when that is wrong, so a tree of thousands of map tiles is not walked each time.
  • Overlays.
    • docker-compose.e2e.yml replaces migrate's volumes wholesale (!override), which would have dropped the mount, so it gains the mount too.
    • The dev and images overlays inherit it and need nothing.
  • make migrate-safe runs the same migrate command, so it repairs the volume on that path as well.
  • Private media is unchanged. The frontend's read-only mount and nginx's /media/private/ guard are untouched.

Changelog

  • changelog.d/1064-media-volume-ownership.fixed.md

Test plan

Proved on docker-test in a throwaway compose project with no host ports; the dev stack was not touched.

  • A root-owned volume holding a file: after up, migrate logged the repair line, /app/media was 999:999, and a write as appuser in the backend succeeded.
  • The cheap path: with 3,000 files and the top level already right, a rerun printed no repair line, and a deliberately root-owned file deep in the tree stayed root-owned, so no recursive walk happened.
  • The migrate-safe path (docker compose run --rm --no-deps -T migrate) repaired a root-owned tree.
  • A fresh volume: no repair line.
  • New test_compose_migrate_media_ownership.py: the mount is present, the stat precedes the chown and both precede Alembic, and the e2e overlay keeps the mount. 7 passed with the e2e bind-mount test.
  • ruff clean; version sync and changelog checks OK.
  • Full suites: CI.

A correction to #1064's report. An empty root-owned volume does not reproduce the bug: Docker re-initialises an empty named volume from the image when it is mounted, so with an image that has #914 it comes back owned by appuser unaided. The failure needs a volume that already holds files. Production's media volume is root-owned and empty, so the 4.5.0 image should initialise it correctly even without this change. This fix covers installs whose volume is not empty, and makes the outcome certain either way.

🤖 Generated with Claude Code

Closes #1064. ## Summary #914 made the image create `/app/media` owned by `appuser`, which only helps a volume Docker initialises from that image. A `media` volume that already holds files and is root-owned stays that way, and the backend cannot write uploads into it. The `migrate` service already runs as root before the app starts, to prepare `./backups` (#414). It now also mounts `media` and repairs it: ``` if [ "$(stat -c %u /app/media)" != "$(id -u appuser)" ]; then echo 'migrate: /app/media is not owned by appuser; repairing (#1064)' chown -R appuser:appuser /app/media fi ``` - **Cost.** One `stat` of the top-level owner on every start. The recursive chown runs only when that is wrong, so a tree of thousands of map tiles is not walked each time. - **Overlays.** - `docker-compose.e2e.yml` replaces `migrate`'s volumes wholesale (`!override`), which would have dropped the mount, so it gains the mount too. - The dev and images overlays inherit it and need nothing. - **`make migrate-safe`** runs the same `migrate` command, so it repairs the volume on that path as well. - **Private media is unchanged.** The frontend's read-only mount and nginx's `/media/private/` guard are untouched. ## Changelog - [x] `changelog.d/1064-media-volume-ownership.fixed.md` ## Test plan Proved on docker-test in a throwaway compose project with no host ports; the dev stack was not touched. - [x] **A root-owned volume holding a file:** after `up`, `migrate` logged the repair line, `/app/media` was `999:999`, and a write as `appuser` in the backend succeeded. - [x] **The cheap path:** with 3,000 files and the top level already right, a rerun printed no repair line, and a deliberately root-owned file deep in the tree stayed root-owned, so no recursive walk happened. - [x] **The `migrate-safe` path** (`docker compose run --rm --no-deps -T migrate`) repaired a root-owned tree. - [x] **A fresh volume:** no repair line. - [x] New `test_compose_migrate_media_ownership.py`: the mount is present, the `stat` precedes the chown and both precede Alembic, and the e2e overlay keeps the mount. 7 passed with the e2e bind-mount test. - [x] ruff clean; version sync and changelog checks OK. - [ ] Full suites: CI. **A correction to #1064's report.** An *empty* root-owned volume does not reproduce the bug: Docker re-initialises an empty named volume from the image when it is mounted, so with an image that has #914 it comes back owned by `appuser` unaided. The failure needs a volume that already holds files. Production's `media` volume is root-owned and empty, so the 4.5.0 image should initialise it correctly even without this change. This fix covers installs whose volume is not empty, and makes the outcome certain either way. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix: migrate repairs a root-owned media volume (#1064)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m11s
CI / Backend lint (ruff) (pull_request) Successful in 35s
CI / Bot/backend version sync (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 1m18s
CI / Does this change touch the recording path? (pull_request) Successful in 36s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 2m26s
CI / Docker image build (pull_request) Successful in 2m48s
CI / Frontend (shard 2/2) (pull_request) Successful in 10m3s
CI / Frontend (shard 1/2) (pull_request) Successful in 12m9s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 14m4s
CI / End-to-end journeys (desktop) (pull_request) Successful in 14m15s
CI / End-to-end journeys (phone) (pull_request) Successful in 18m47s
CI / Backend migration, tests, and audit (pull_request) Successful in 29m12s
dcaea09da4
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-bot deleted branch fix/1064-media-volume-ownership 2026-10-04 04:20:16 +00:00
rbrooks referenced this pull request from a commit 2026-10-04 06:57:14 +00:00
Sign in to join this conversation.
No description provided.