build(afm): run dev/CI containers as a non-root user#44
Merged
Conversation
The dev image ran as root, so every file a container wrote into the
/workspace bind mount (generated artefacts, wasm pkg/, mdbook output,
node_modules) landed root-owned on the host — the classic bind-mount
footgun, and not how a dev container should behave.
Create a non-root `dev` user (UID/GID default 1000, overridable via
`--build-arg UID=/GID=`) in the dev and book image stages, chown the
/cargo cache dirs and the playground node_modules mountpoint so fresh
named volumes initialise dev-owned, and `USER dev` at the end of each
stage (the fuzz stage flips back to root for its /usr/local installs,
then drops to dev again).
Runtime UID is selected by `user: "${AFM_UID:-1000}:${AFM_GID:-1000}"`
on the dev/fuzz/ci/book/playground services: unset locally → the
host-matched 1000 (host-owned writes); CI sets AFM_UID=0 (setup-dev-image
action + the book job env) → root, because the ephemeral runner's
checkout is owned by a different UID and a non-root container cannot
create files inside it. CI thus stays byte-identical to the proven
root behaviour while local dev becomes non-root.
Pre-existing root-owned cargo named volumes migrate in place with a
one-off `docker compose run --user 0:0 dev chown -R 1000:1000 /cargo`
(no cache loss); fresh volumes need none.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The dev image ran as root, so every file a container wrote into the
/workspacebind mount (generated artefacts, wasmpkg/, mdbook output,node_modules) landed root-owned on the host — the classic bind-mountfootgun, and not how a dev container should behave.
This switches the dev/CI containers to a non-root user matched to the host
UID, the industry-standard pattern.
How
dev+bookstages): create a non-rootdevuser(UID/GID default 1000, overridable via
--build-arg UID=/GID=),chownthe/cargocache dirs and the playgroundnode_modulesmountpoint so fresh named volumes initialise dev-owned, and
USER devat the end of each stage. The
fuzzstage flips back toUSER rootforits
/usr/localinstalls (nightly + cargo-fuzz/udeps), then drops todevagain.user: "${AFM_UID:-1000}:${AFM_GID:-1000}"on thedev/fuzz/ci/book/playgroundservices. Unset locally → thehost-matched 1000 (host-owned writes); CI sets
AFM_UID=0→ root.bookCI job: emitAFM_UID=0/AFM_GID=0so CI stays byte-identical to the proven root behaviour — the
ephemeral runner's checkout is owned by a different UID and a non-root
container can't create files (
pkg/,book/,dist/) inside it.Rationale for keeping CI as root
CI is ephemeral and ownership is throwaway there; the runner's checkout is
owned by a different UID than any baked image user, so root is the
simplest correct choice and avoids a cross-UID write-permission class of
failure. Local dev — where the bind mount is the developer's real tree — is
where non-root matters, and that's now fixed.
Verification (local, UID 1000)
docker compose run --rm dev id→uid=1000(dev);AFM_UID=0 … id→root.yasunobu:yasunobu-owned (was root).chown -R 1000 /cargo, no cache loss); freshplayground-node-modulesvolume initialises dev-owned.just cigreen non-root (pre-push gate);just book-buildwrites host-ownedbook/.🤖 Generated with Claude Code