Problem
Apps that bind-mount {{ fs.app_data }} and write files at runtime currently must run as root in their container, because Docker creates the bind-mount source dir as UID 0 and no chown step runs before compose-up.
Concrete cases:
- syncthing (PR #2) — official image runs UID 1000, no chown entrypoint. Fails with
save cert: open /var/syncthing/config/cert.pem: permission denied. Workaround: user: "0:0".
- vikunja — already ships
user: 0:0 for the same reason.
Running app containers as root is bad on two axes:
- Breakout blast radius. Container escape → root on host paths under
fs.app_data and fs.shared.
- File ownership pollution in
fs.shared. Files written as UID 0 by one app become read-only / chown-required for other apps that legitimately run unprivileged.
This pattern will spread as more apps are added unless we fix it at the platform level.
Proposal
Freeshard creates {{ fs.app_data }}/<app> (and ensures {{ fs.shared }}) with a known, non-root owner before running docker compose up. App templates then declare user: \"1000:1000\" (or rely on the image default) and write happily.
Two flavors:
Minimal — single platform-wide app UID
- Pick a standard UID/GID (e.g.
1000:1000).
- On app install,
mkdir -p and chown the per-app app_data dir to that UID:GID.
- Document the convention; templates use
user: \"1000:1000\".
Pros: one core change, fixes the common case. Cons: doesn't help images whose internal user is a different UID.
Optional extension — per-app UID hint
- Add optional field to
app_meta.json schema:
\"runtime\": { \"uid\": 1001, \"gid\": 1001 }
- Install pipeline chowns to those values if set, else falls back to the platform default.
Pros: covers images like linuxserver/* (UID 911) without forcing a workaround. Cons: schema bump, more surface area.
I'd start with the minimal version and only add the per-app hint when the first app needs it.
Where the change lives
shard_core/service/app_installation/ — the install pipeline already constructs the app_data path (util.py:82). Add a step that ensures the dir exists with the chosen ownership before compose-up.
Migration
Existing installs already have app_data dirs owned by root. The chown step should be idempotent and run on every install/upgrade, not just first install, so existing shards heal on next deploy.
Acceptance
- New app installs create
app_data owned by the chosen UID:GID.
- syncthing template can drop
user: \"0:0\" and run as 1000:1000.
- vikunja template can drop
user: 0:0.
- No regressions for apps that don't bind-mount
app_data.
Problem
Apps that bind-mount
{{ fs.app_data }}and write files at runtime currently must run as root in their container, because Docker creates the bind-mount source dir as UID 0 and no chown step runs before compose-up.Concrete cases:
save cert: open /var/syncthing/config/cert.pem: permission denied. Workaround:user: "0:0".user: 0:0for the same reason.Running app containers as root is bad on two axes:
fs.app_dataandfs.shared.fs.shared. Files written as UID 0 by one app become read-only / chown-required for other apps that legitimately run unprivileged.This pattern will spread as more apps are added unless we fix it at the platform level.
Proposal
Freeshard creates
{{ fs.app_data }}/<app>(and ensures{{ fs.shared }}) with a known, non-root owner before runningdocker compose up. App templates then declareuser: \"1000:1000\"(or rely on the image default) and write happily.Two flavors:
Minimal — single platform-wide app UID
1000:1000).mkdir -pandchownthe per-appapp_datadir to that UID:GID.user: \"1000:1000\".Pros: one core change, fixes the common case. Cons: doesn't help images whose internal user is a different UID.
Optional extension — per-app UID hint
app_meta.jsonschema:\"runtime\": { \"uid\": 1001, \"gid\": 1001 }Pros: covers images like
linuxserver/*(UID 911) without forcing a workaround. Cons: schema bump, more surface area.I'd start with the minimal version and only add the per-app hint when the first app needs it.
Where the change lives
shard_core/service/app_installation/— the install pipeline already constructs theapp_datapath (util.py:82). Add a step that ensures the dir exists with the chosen ownership before compose-up.Migration
Existing installs already have
app_datadirs owned by root. The chown step should be idempotent and run on every install/upgrade, not just first install, so existing shards heal on next deploy.Acceptance
app_dataowned by the chosen UID:GID.user: \"0:0\"and run as1000:1000.user: 0:0.app_data.