diff --git a/docs/per-user-linux-accounts.md b/docs/per-user-linux-accounts.md index 15ed736a..a919da83 100644 --- a/docs/per-user-linux-accounts.md +++ b/docs/per-user-linux-accounts.md @@ -328,6 +328,61 @@ readable by them, `authorized_keys` byte-identical to what was pasted, the key * run (it has been added to Gitea by then), and a multi-line paste refused with `authorized_keys` left untouched. +## Docker: rootless, one daemon per member + +Verified working on a real member account, 2026-08-11. + +**Not the `docker` group.** `usermod -aG docker ` is the one-line version and it is root: membership +means talking to the host daemon, which runs as root, so `docker run -v /:/host -it alpine chroot /host` is +a root shell. That reads `.env`, every other member's home and the wallet seed — every boundary above, +bypassed by one documented command. The group is not "access to Docker", it is "root, by a longer route". + +Rootless gives what was actually wanted: a daemon per account, containers in that account's user namespace, +images under their own home. Measured — the daemon runs as the member, `docker pull` put 403 MB in their +home, and `docker ps -a` showed nothing while the owner had four containers running. + +**Host prerequisites**, all in `setup.sh` as core packages: `uidmap` (newuidmap/newgidmap — rootless cannot +start without them), `dbus-user-session`, and Docker's own rootless extras. `useradd` allocates the +`/etc/subuid` range automatically wherever `login.defs` sets `SUB_UID_COUNT`, and `userdel` reclaims it. + +**`loginctl enable-linger` is required, not optional.** Officer's shells are not login sessions, so without +it a member's daemon would stop the moment their terminal closed. + +**The setup tool's exit code is not the gate.** It writes `~/.config/systemd/user/docker.service` and then +fails its own `systemctl --user start` with "Unit docker.service not found", because nothing reloaded a +manager that was already running. So: run it, `daemon-reload`, start it ourselves, and verify by asking the +daemon its version. + +**Two features built the same day collided.** Creating a volume copies xattrs, and the DEFAULT ACLs on a +member's home — added so the file browser could read their files — are inherited by Docker's storage, where +a mapped id inside a user namespace is not a valid id to set: + +``` +failed to copy xattrs: failed to set xattr "system.posix_acl_default" on …/volumes/…/_data: invalid argument +``` + +Every container failed to start while the image pulled perfectly. The fix strips DEFAULT ACLs from +`~/.local/share/docker` only (`setfacl -R -k`), leaving the access ACLs the file browser depends on. Losing +the platform's reach into Docker's internal storage costs nothing: it is layers and volume data, read +through `docker` or not at all. + +### The port space is shared, and that is not fixed + +A rootless daemon is isolated; the **host's port space is not**. RootlessKit publishes into it, so a member +mapping `5432` collides with the owner's production Postgres — observed immediately: + +``` +error while calling RootlessKit PortManager.AddPort(): listen tcp4 0.0.0.0:5432: bind: address already in use +``` + +Two consequences worth knowing: + +- **Publish on `127.0.0.1` explicitly.** A bare `-p 15432:5432` binds `0.0.0.0` in rootless mode, putting a + member's dev database on the network. `127.0.0.1:15432:5432` is all they need to reach it from their own + shell. +- **Nothing allocates ports.** With one member the owner manages it by hand, which is where this stands + deliberately. With several, a per-member offset is the crude answer that works. + ## Follow-ups this creates - **Deleting a member no longer removes their home.** It belongs to their uid, so the platform cannot