Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
REPORT — userdata layout + shared-storage ownership convention → controller v0.66.1 + catalog (2026-06-14)
Customer-facing userdata/ tree with a shared-ownership convention so FileBrowser + content apps
collaborate without permission collisions. Spike:
felhom.eu/documentation/audits/SPIKE-userdata-layout-2026-06-14.md. Trunk-based on main in both the
controller and the app-catalog. No back-compat (existing demo apps moved to the new layout — no real
data). Built + deployed + live-verified on demo guest 9201.
Validate against pushed source at file:line, not this report.
Confirmed baseline
- Controller
mainwascbaa53f= v0.65.0 → shipped v0.66.0 then v0.66.1 (first-deploy fix). - Catalog
mainwas5ec2b2c(commits, no semver).
Commit hashes
| Repo | Commit | Scope |
|---|---|---|
| app-catalog | 69611ce |
repoint 12 composes off ${HDD_PATH}/media → ${USERDATA_PATH}/... + .felhom.yml |
| app-catalog | 0d60a5c |
komga + audiobookshelf revert to root (user:1000 fallback) |
| app-catalog | c99070c |
komga + audiobookshelf no-new-privileges:true (root hardening) |
| controller | c48f95f |
v0.66.0 — userdata helper, USERDATA_PATH inject, skeleton, deploy belt, FileBrowser switch, migrate setgid fix + tests |
| controller | 3b7d089 |
v0.66.1 — fix USERDATA_PATH on first deploy (shared withUserdataPath) + regression test |
What shipped
- Catalog:
${USERDATA_PATH}introduced; media/ingest mounts repointed per app (jellyfin/emby/plexmedia:/media:ro; navidromemedia/music:ro; audiobookshelfmedia/{audiobooks,podcasts}; komgamedia/comics; calibremedia/books+import/calibre,UMASK=002; radarrmedia/movies+downloads, sonarrmedia/tv+downloads, bothUMASK=002; romm library→userdata/roms; immich +externalmedia/photos:/external/photos:ro; paperless consume→import/paperless). nextcloud unchanged. - Controller (
internal/appbackup/userdata.go):EnsureUserdataDir/EnsureDirOwned(MkdirAll → explicitChmod(ModeSetgid|0775)→ chown gid 1000),UserdataSkeleton,EnsureUserdataSkeleton; LinuxchownGID/StatGID+ non-Linux stubs.withUserdataPathinjectsUSERDATA_PATH=<HDD_PATH>/userdatain BOTH env builders. Skeleton pre-create on register + FB sync; deploy belt incomposeExecCustomEnv(up). FileBrowser mountsuserdata(uid 1000).migrate.gopreserves setgid+group (#8).
komga / audiobookshelf — user:1000 outcome: FELL BACK TO ROOT
Tried user: "1000:1000" on both (catalog 0d60a5c). Both crash-looped — their named config/metadata
volumes are Docker-created root-owned and the pinned-1000 process can't write them (live: komga SQLite
/config open failure, 14 restarts; audiobookshelf EACCES mkdir '/metadata/logs'; neither image has a
PUID-style root-init chown). Reverted to root (the spec's accepted fallback) + added
no-new-privileges:true (catalog c99070c) to restore an escalation boundary (full cap_drop NOT
applied — the root-init needs CHOWN/SETUID/SETGID). Both then deploy healthy as root (audiobookshelf
healthy; komga serving — see healthcheck note). Consequence: root+setgid → their output is group-
readable (FileBrowser browses/reads), not group-writable; the app manages its own files. PUID-1000 apps
(radarr/sonarr/calibre + UMASK 002) give full group-write.
immich / paperless — DEPLOY DEFERRED (guest RAM), design verified
Guest 9201 has 2 GiB RAM total. immich needs ~4 GiB → cannot deploy. paperless (~1.1 GiB:
app+postgres+redis) does not fit alongside the running apps; the controller's memory gate refuses it
(observed: komga refused at 92 MB free until non-media apps were temporarily stopped). Their compose +
run-identity design is shipped + statically verified (paperless USERMAP 1000 consumes from
import/paperless; immich media/photos:/external/photos:ro). immich external library: MOUNT READY,
REGISTRATION PENDING — making /external/photos visible is only the mount; surfacing it in Immich
requires registering an External Library at Administration → External Libraries (admin UI/API), a
post-deploy step compose cannot do. Flagged in the immich compose + .felhom.yml first_steps. Not
overclaimed.
Live-verify evidence (guest 9201) — all three fixes PASS
- (c) convention:
ls -la/statof/mnt/felhom-usb/userdataand every subdir → mode 2775 (setgid) group 1000 throughout (media/{movies,comics,...}, roms, import/{paperless,calibre}, downloads, documents). Same on the system drive. - (a) FileBrowser writes userdata:
docker exec filebrowser(uid 1000)mkdir+ write a file under/srv/felhom-usb/documents/...→MKDIR_OK/WRITE_OK, new dir inherited gid 1000 via setgid. The original permission-denied (root-ownedappdatamount) is gone. - (b) shared media across apps: radarr (uid 1000, UMASK 002) wrote
media/movies/Inception/Inception.mkv→ dir 2775, file 664 group 1000; jellyfin (root,media:/media:ro)catit →MOVIEDATA; FileBrowser (uid 1000) listed and deleted it → full RW collaboration. komga (root fallback) wrotemedia/comics/Batmangroup 1000 → FileBrowser lists/reads but cannot delete (the documented root-fallback group-read tradeoff). - App health after moving to the new layout: jellyfin, calibre-web, romm, radarr, audiobookshelf all
Up healthy; komga serving (HTTP up, tasks running, writes/reads OK) but its Docker healthcheck
(
/api/v1/actuator/health) reports unhealthy — a pre-existing healthcheck-probe nuance in the komga template, NOT a userdata/permission issue (no OOM, 0 restarts, logs clean). Flagged for a separate healthcheck fix.
Tests (per-commit green; authoritative Linux gate on 192.168.0.180)
internal/appbackup: SharedContentGID==1000, skeleton list+structure, EnsureDirOwned setgid+gid
(Linux; in-test companion proves a plain MkdirAll has NO setgid). internal/stacks:
ParseComposeUserdataMounts selectivity, deploy belt creates declared dirs, withUserdataPath
on/off-by-HDD_PATH, migration preserves setgid+group (Linux — mutation-proven: reverting to
0755/.Perm() drops setgid → test FAILS). Full go build && go vet && go test ./... green.
Follow-ups (small, noted not done)
- FileBrowser-created dirs are 2755 (its umask 022) — group-read not group-write for OTHER actors;
consider a FileBrowser
UMASK/user:1000:1000-with-002 so customer-made folders are group-writable. - komga healthcheck endpoint (
/api/v1/actuator/health) returns non-200 on komga 1.20 → "unhealthy" despite serving; fix the healthcheck definition. - immich/paperless live deploy needs a larger guest (the 2 GiB demo can't host them).