the rootless docker fix is proven; correcting the record
3bea46f said running a container was unverified and the ACL fix unproven. Both are now
verified on a real member account: the container that previously died copying xattrs starts,
which means volume creation gets past system.posix_acl_default.
Documented in docs/per-user-linux-accounts.md rather than left in a commit message — why the
docker group is root and not an option, the host prerequisites, why linger is required, why
the setup tool's exit code cannot be the gate, and the ACL collision between the file
browser's default ACLs and Docker's volume creation.
Also written down because it bit within a minute of the feature working: a rootless daemon
is isolated but the HOST port space is not. RootlessKit publishes into it, so a member
mapping 5432 collides with the owner's production Postgres. Publish on 127.0.0.1 explicitly
— a bare -p binds 0.0.0.0 in rootless mode, which puts a member's dev database on the
network. Nothing allocates ports; with one member that is the owner's job by hand, and that
is where it stands deliberately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <user>` 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
|
||||
|
||||
Reference in New Issue
Block a user