Skip to content

chore(devcontainer): make rootless podman work here - #716

Merged
pofallon merged 2 commits into
mainfrom
chore/devcontainer-podman
Aug 28, 2026
Merged

chore(devcontainer): make rootless podman work here#716
pofallon merged 2 commits into
mainfrom
chore/devcontainer-podman

Conversation

@pofallon

Copy link
Copy Markdown
Contributor

podman was installed ad hoc in a running container and would vanish on the next rebuild. Nothing in the config asked for it, and nothing made it work. This makes it declarative, so the Podman CI lane can be reproduced locally instead of only in CI.

Three things were false by default. Each was measured, not assumed.

1. Subordinate IDs that fit the ambient user namespace

This devcontainer already runs inside a userns/proc/self/uid_map reads 0 100000 65536, so only ids 0..65535 exist for us. The image ships vscode:100000:65536, naming ids this namespace cannot represent:

newuidmap: write to uid_map failed: Operation not permitted

Podman additionally refuses any single range containing the user's own uid, so the allocation has to straddle it as two ranges. Size matters past merely starting a container — a range stopping short of 65534 cannot map nobody, and Feature installs then die inside apt:

E: setgroups 65534 failed - setgroups (22: Invalid argument)

The script derives 1:999 + 1001:64535 from uid_map rather than hardcoding, so it also does the right thing on a host with no ambient remap.

2. A storage driver the kernel will actually mount

Rootless overlay goes through fuse-overlayfs, which needs /dev/fuse. The module is loaded (fuse is in /proc/filesystems) but the node isn't exposed to a container unless requested, and mknod is refused from inside. The script picks overlay when the node exists, vfs otherwise.

/dev/fuse is documented as an opt-in, not set in runArgs. It grants the container a host device purely for a speed-up, and correctness doesn't depend on it — everything below was verified on vfs, with no added container privileges.

3. An API socket — for compose only

podman compose execs an external provider and points it at podman by exporting DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock (measured by pointing compose_providers at a script that dumps its environment; it also sets DOCKER_BUILDKIT=0 and blanks DOCKER_CONFIG). With nothing listening, every compose call fails with Cannot connect to the Docker daemon. There's no systemd here to socket-activate it, so postStartCommand runs it directly.

Verified

  • rootless podman run works
  • podman compose build puts its image in podman's own store, where podman tag finds it
  • DEACON_CONTAINER_RUNTIME=podman cargo nextest run runs the suite for real
  • both scripts re-run cleanly (idempotency checked, not assumed)

This is what let me measure #715 against a genuine podman substrate rather than reasoning about it — including finding that the Podman CI lane needs the socket started, which it currently doesn't do.

🤖 Generated with Claude Code

https://claude.ai/code/session_016SFzA2sTh2EpX8MZU3TWNS

…ne is reproducible locally

podman was installed ad hoc in a running container and would vanish on the next
rebuild. Nothing in the config asked for it, and nothing made it work: three
things were false by default, each measured rather than assumed.

SUBORDINATE IDs THAT FIT THE AMBIENT USERNS. This devcontainer already runs
inside a user namespace — /proc/self/uid_map reads `0 100000 65536`, so only ids
0..65535 exist for us — while the image ships `vscode:100000:65536`, naming ids
this namespace cannot represent. newuidmap fails with `write to uid_map failed:
Operation not permitted`. Podman also refuses any single range containing the
user's own uid, so the allocation has to straddle it as two ranges. Size matters
past merely starting a container: a range stopping short of 65534 cannot map
`nobody`, and Feature installs then die inside apt with `setgroups 65534 failed`.
The script computes 1:999 + 1001:64535 from uid_map rather than hardcoding it, so
it also does the right thing on a host with no ambient remap.

A STORAGE DRIVER THE KERNEL WILL MOUNT. Rootless overlay goes through
fuse-overlayfs, which needs /dev/fuse. The module is loaded but the node is not
exposed to a container unless requested, and mknod is refused from inside. The
script picks overlay when the node exists and vfs otherwise. vfs needs no added
container privileges, which is why /dev/fuse is documented as an opt-in rather
than set in runArgs: it grants a host device for a speed-up, and correctness does
not depend on it.

AN API SOCKET, for compose only. `podman compose` execs an external provider and
points it at podman by exporting DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/…
(measured by pointing compose_providers at a script that dumps its environment;
it also sets DOCKER_BUILDKIT=0 and blanks DOCKER_CONFIG). With nothing listening,
every compose call fails with `Cannot connect to the Docker daemon`. No systemd
here to socket-activate it, so postStartCommand runs it directly.

Verified end to end: rootless `podman run` works, `podman compose build` puts its
image in podman's own store where `podman tag` finds it, and
`DEACON_CONTAINER_RUNTIME=podman` now runs the suite for real. Both scripts are
idempotent and were re-run to prove it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SFzA2sTh2EpX8MZU3TWNS
…fails

Both hooks ran unguarded, so a host with no apt mirror, no passwordless sudo, or
a kernel that will not give podman a usable id mapping would fail the whole
devcontainer build — disproportionate for an optional convenience on a repo whose
default runtime is docker.

The scripts keep `set -euo pipefail` so the failure stays loud and debuggable;
only the container's fate is decoupled from it. `podman --version` also comes
back out of postCreateCommand for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SFzA2sTh2EpX8MZU3TWNS
@pofallon

Copy link
Copy Markdown
Contributor Author

Pushed 6e8a60f: both lifecycle hooks are now || echo-guarded.

As first written they were unguarded, so a host with no apt mirror, no passwordless sudo, or a kernel that won't give podman a usable id mapping would have failed the entire devcontainer build. That's disproportionate for an optional convenience on a repo whose default runtime is docker — and it would have hit people who never asked for podman.

The scripts keep set -euo pipefail, so the failure is still loud and debuggable; only the container's fate is decoupled from it. podman --version comes back out of postCreateCommand for the same reason.

@pofallon
pofallon merged commit a1bffc2 into main Aug 28, 2026
15 checks passed
@pofallon
pofallon deleted the chore/devcontainer-podman branch August 28, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Chore & maintenance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant