Run containers as the host user instead of root
Everything the containers wrote into the bind mounts (surreal_data, notebook_data) was owned by root, so the host user could not delete or back up his own podcast data — prune_podcast_data.py and update_stack.sh had to detour through `docker compose exec` for every rm and tar. That was not a requirement, just the default: the open_notebook image declares no USER, and the compose file even overrode surrealdb's own non-root user (65532) with `user: root`, under the comment "Required for bind mounts on Linux" — which is not true. Both services now run as user: "1000:1000". Two things this needs: - HOME=/tmp for open_notebook. Without it HOME resolves to "/" for a non-root uid, uv cannot create /.cache/uv, and api + worker exit 2 at startup. Verified by running the image as uid 1000 both ways. - The data directories must be owned by that uid. Existing data was adopted with a throwaway root container (chown -R), no sudo needed. Both scripts drop the container detour and operate on the host directly, which is simpler and now honest. smoke_test.sh gained two checks so a silent regression to root cannot go unnoticed: the container's uid must match the host user, and no foreign-owned files may exist under the data directories. Verified: containers run as uid 1000, new files land as dschlueter and are deletable without sudo, SurrealDB writes as 1000, a source can be created, embedded and deleted through the API, and the full smoke test is green. Note this deviates from what the image expects (it assumes root), so it is exactly the kind of assumption an update can break — hence the smoke-test checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
09c6d910a7
commit
9e6d790830
6 changed files with 96 additions and 40 deletions
36
CLAUDE.md
36
CLAUDE.md
|
|
@ -46,13 +46,40 @@ Updating (`scripts/update_stack.sh`): resolve the new digest → stop → back u
|
|||
`smoke_test.sh` → **roll back data and compose file if the smoke test fails**. The data backup is
|
||||
the point: the app migrates the database on startup, and an older application generally cannot
|
||||
read a migrated database, so a bad update would otherwise be a one-way door. Backup/restore runs
|
||||
`tar` **inside a container** because the data directories are root-owned (the container writes as
|
||||
root) and the host user cannot restore over them.
|
||||
plain `tar` on the host, which works because the containers run as the host user (see *Container
|
||||
user* below).
|
||||
|
||||
Both scripts read the digest from the `lfnovo/open_notebook` line specifically — a naive "first
|
||||
sha256 in the file" grep picks up **surrealdb** (it is listed first), which would make the update
|
||||
script rewrite the database image instead of the application.
|
||||
|
||||
## Container user
|
||||
|
||||
Both services run as the **host user** (`user: "1000:1000"`), not as root, and `surreal_data/` +
|
||||
`notebook_data/` are owned by that uid.
|
||||
|
||||
Default would be root: the `open_notebook` image declares no `USER`, and the compose file used to
|
||||
override surrealdb's own non-root user (`65532`) with `user: root` under the (wrong) comment
|
||||
"required for bind mounts on Linux". Everything a root container writes into a bind mount is
|
||||
root-owned, so the host user could not delete or back up its own podcast data — every cleanup had
|
||||
to detour through `docker compose exec`.
|
||||
|
||||
Two things this needs, and both are load-bearing:
|
||||
|
||||
- **`HOME=/tmp` for `open_notebook`.** Without it, `HOME` resolves to `/` for a non-root uid, `uv`
|
||||
fails to create `/.cache/uv` (permission denied), and api + worker die at startup with exit 2.
|
||||
The failure is immediate and loud, but the cause is not obvious from the message.
|
||||
- **The data directories must already be owned by that uid.** On a fresh checkout `mkdir -p
|
||||
surreal_data notebook_data` is enough. To adopt existing root-owned data without `sudo`:
|
||||
```bash
|
||||
docker run --rm -u 0 -v "$PWD:/work" -w /work --entrypoint chown \
|
||||
lfnovo/open_notebook:v1-latest -R 1000:1000 surreal_data notebook_data
|
||||
```
|
||||
|
||||
Note this is a deviation from what the image expects (it assumes root), so it is exactly the kind of
|
||||
thing an update can break. `smoke_test.sh` therefore asserts the container's uid matches the host
|
||||
user and that no foreign-owned files exist under the data directories.
|
||||
|
||||
### Why a smoke test, and not just "does it start"
|
||||
|
||||
Every local adaptation in this repo leans on upstream internals, and an update can break them
|
||||
|
|
@ -142,8 +169,9 @@ image updates (the same trap the existing overlays already carry). Instead:
|
|||
apply) reconciles the episode list from the API against the directories on disk and removes orphans plus
|
||||
the `clips/` of completed episodes. Two guards matter — it aborts if any job is `running`/`pending`
|
||||
(a running job has no `audio_file` yet, so its directory is indistinguishable from an orphan) and it
|
||||
skips directories touched in the last 60 minutes. **Deletion runs inside the container** (`docker compose
|
||||
exec … rm -rf`), because the container writes as root and the host user can't remove those directories.
|
||||
skips directories touched in the last 60 minutes. Deletion is a plain `shutil.rmtree` on the host —
|
||||
possible because the containers run as the host user (see *Container user*); as root-owned data it
|
||||
would need a container detour.
|
||||
|
||||
### YouTube sources — German transcripts need `CCORE_CONFIG_PATH`
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue