Mapping UIDs for Rootless Podman DevContainers

Under rootless Podman, files a container writes to your workspace can land owned by a subordinate UID instead of you. This page fixes that with the keep-id user-namespace mapping, so container writes stay owned by your host user and Git keeps working.

This matters because rootless Podman does not run containers as your login user. It runs them inside a user namespace where UID 0 in the container is mapped to your real host UID, and every other UID inside the container is mapped up into the subordinate range declared in /etc/subuid. That indirection is exactly what makes rootless secure — a process claiming to be root inside the container is really an unprivileged subordinate ID on the host — but it also means a vscode user with UID 1000 inside the container lands somewhere far up your subordinate range on the host. When that container writes to a bind-mounted workspace, the resulting files carry the mapped owner, and stat on the host reports an unfamiliar five- or six-digit UID rather than your own.

You reach for keep-id whenever a rootless container needs to read and write files on a bind mount that your host user also touches: an editor saving source, a language server writing caches, a build emitting artifacts back into the tree, or Git staging changes you made from both sides. The mental model is a single deliberate exception to the default mapping. Instead of pushing your interactive container user up into the subordinate range, keep-id pins it back down to your host UID and GID, while every other identity in the container continues to use the subordinate range as normal. Get that one mapping right and the ownership churn — the chown -R scripts, the "detected dubious ownership" warnings from Git, the permission-denied writes — simply stops happening.

Prerequisites

You need rootless Podman with subuid/subgid ranges configured.

  • Podman running rootless (podman info as your user).
  • /etc/subuid and /etc/subgid with a range for your user.
  • A devcontainer you can add runArgs to.

UID-mapping prerequisitesYou need subuid ranges, the keep-id arg, a build, and an ownership check.subuid rangefor your userkeep-idin runArgsBuildpodmanVerifyfile owner

The prerequisite people most often get wrong is the subordinate range itself. Running podman info successfully is not proof that /etc/subuid and /etc/subgid hold an entry for your user — Podman can start rootless with a minimal or default mapping and still fail to map keep-id correctly. Each line in those files reads username:startid:count, and the count has to be large enough to cover the highest UID any process inside the container will assume. A default range of 65536 is comfortable for a single interactive user, but a truncated range — say a few hundred IDs left over from a hand-edited file — is exactly what makes keep-id appear to do nothing.

One more detail matters before you start: the remoteUser you intend to keep-id must actually exist inside the image with a stable UID. keep-id maps whichever user the container runs as, so if remoteUser is vscode the image needs a vscode account, and its in-container UID is what Podman remaps to your host UID. If you added subuid or subgid ranges by hand, run podman system migrate once so Podman picks up the new ranges rather than reusing a stale namespace from a previous run.

Step-by-Step Implementation

  1. Confirm your subordinate ID range exists.
grep "^$USER:" /etc/subuid /etc/subgid

This grep anchors on ^$USER: so it matches only the line that starts with your exact username, not a substring of some other account, and it checks /etc/subuid and /etc/subgid together because Podman needs both to build the mapping. A healthy result prints two lines of the form you:100000:65536, telling you the range begins at 100000 and spans 65536 IDs. If either file returns nothing, keep-id has no room to place your other identities and the build will fail with a subuid-related error rather than silently mismapping — which is why confirming this first saves a confusing round of debugging later.

  1. Add the keep-id userns mapping so the container user maps to your host UID.
{
  "runArgs": ["--userns=keep-id"],
  "remoteUser": "vscode"
}

runArgs passes flags straight through to podman run, so --userns=keep-id reaches the engine unaltered and requests the special mapping at container creation time. Pairing it with remoteUser is deliberate: remoteUser decides which account the devcontainer tooling runs commands as, and keep-id maps that specific account down to your host identity. Set one without the other and the two can disagree — the tooling runs as vscode while the mapping keeps a different user, and you are back to files owned by an ID you do not recognise. Keeping both in devcontainer.json also means the mapping travels with the project, so a teammate cloning the repo gets the same ownership behaviour without re-discovering the flag.

  1. Rebuild and let Podman apply the mapping.
devcontainer up --workspace-folder . --remove-existing-container

The --remove-existing-container flag is doing the heavy lifting here. A user-namespace mapping is fixed when the container is created; you cannot change it on a running container, so an incremental restart that reuses the old container would keep the old mapping and make it look as though keep-id never took effect. Forcing a fresh creation guarantees Podman reads the updated runArgs and applies the new --userns=keep-id mapping from scratch. This is the single most common reason people conclude the flag "did nothing" — they edited devcontainer.json but reattached to a stale container that was built before the change.

  1. Verify a container-created file is owned by you.
devcontainer exec --workspace-folder . -- touch /workspace/.owncheck && stat -c '%U' .owncheck

This one-liner proves the mapping end to end. The container side runs touch to create /workspace/.owncheck, which lands in the bind-mounted workspace, and then the host side runs stat -c '%U' against the same file from your own shell. Splitting the check across the namespace boundary is the point: the file is written by the container user but inspected by the host, so %U resolving to your own username is direct confirmation that keep-id mapped the writer to your host UID. If stat prints a numeric ID instead of a name, that number is unmapped on the host and tells you the mapping is still off — usually a stale container or a subuid range that does not reach the container user's UID. Delete the .owncheck marker once you are satisfied so it does not end up committed.

keep-id mappingkeep-id maps the container user directly to your host UID so files stay yours.Container uservscode / root insidekeep-id map-> your host UIDsubuid rangeother IDs map hereResultworkspace files owned by you

Common Pitfalls

Ownership problems under rootless are almost always a missing or wrong userns mapping.

The trap that catches people most often is a named volume or cache mounted alongside the workspace. keep-id fixes ownership on bind mounts, where the host directory already exists with your ownership, but a named volume that Podman creates fresh starts life owned by whatever the container writes into it first. If you mount a Cargo, npm, or pip cache as a named volume and the container populates it before the mapping settles, you can end up with a cache directory the mapped user cannot rewrite on the next build. When a cache misbehaves under keep-id, check whether it is a bind mount that inherits your ownership or a named volume with its own initial owner, and prefer a bind mount into a host-owned cache path when you need the two sides to share files cleanly.

A second, subtler pitfall is a mismatch between the UID baked into the image and the UID keep-id expects to map. keep-id remaps whatever UID the remoteUser account holds inside the image, so an image built with vscode at UID 1000 maps differently from one where a feature or a later useradd shifted that account to 1001. When ownership looks right on one machine and wrong on another from the same devcontainer.json, the divergent in-container UID is usually the cause — pin the account's UID in the image build rather than trusting the distribution default, so the mapping is identical everywhere the container runs.

UID triageA triage path from wrong ownership to a correct keep-id mapping.Files owned by a subuid, not you?YESAdd --userns=keep-idkeep-id set but still wrong?YESCheck subuid range covers the UIDOwnership is correct

SymptomRoot CauseRemediation
Files owned by a high subuidNo keep-id mappingAdd --userns=keep-id to runArgs
keep-id has no effectsubuid range too smallWiden the subuid/subgid range
Permission denied on mountContainer UID can't writeAlign remoteUser UID with keep-id
Works rootful, not rootlessAssumed root daemon behaviourUse keep-id under rootless

Conclusion

Rootless ownership comes down to one option: --userns=keep-id maps the container user to your host UID, so everything it writes to the workspace stays yours. Confirm your subuid range covers it, and bind-mount ownership stops being a rootless headache.

The strategic payoff is that ownership stops being a per-machine surprise and becomes a property of the project. Because the --userns=keep-id flag and its remoteUser live in devcontainer.json, the mapping is versioned alongside the code and reproduces the same way on every clone, the same way a pinned base image or a locked dependency file does. That is the same reproducibility discipline you apply to the rest of the environment, extended to identity: instead of each contributor discovering the subuid problem and papering over it with a local chown, the repository declares the correct mapping once and everyone inherits it.

It also composes cleanly with the caching strategies you use elsewhere. Once container writes are owned by your host user, a bind-mounted build cache is genuinely shared between the host and the container rather than being a one-way write that the other side cannot update. That is what lets a pinned toolchain, a warm dependency cache, and a rootless engine coexist without a nightly ownership reset — the files a build produces today are the files your next build, from either side of the namespace boundary, is free to reuse tomorrow.

keep-id summarykeep-id keeps files host-owned given a subuid range and matching user.keep-id givesHost-owned filesWorking GitNo chown danceNeedsA subuid rangeMatching remoteUserPodman rootless

FAQ

What does --userns=keep-id actually do? It maps the container's user to your host user's UID/GID directly, instead of into a subordinate range. So a file the container writes to a bind-mounted workspace is owned by your host user, keeping Git and host tooling happy. Under the default rootless mapping that same user would land far up your /etc/subuid range; keep-id carves out one exception so the interactive user is pinned to your real identity while every other UID in the container still uses the subordinate range. The mapping is decided when the container is created, so it applies to every file that user writes for the life of the container.

Why are my files owned by a huge UID like 100999? That is a subordinate UID from your /etc/subuid range — the default rootless mapping. Add --userns=keep-id so the container user maps to your real UID instead, and new files are owned by you. The specific number is your range start plus the container user's UID, so a vscode account at UID 1000 with a range starting at 100000 surfaces as 100999 after the off-by-one for root. Seeing that arithmetic line up is a good sign the default mapping is working exactly as designed — it just is not the mapping you want for a shared workspace.

Do I still need remoteUser with keep-id? Yes. remoteUser selects which container user runs, and keep-id maps that user to your host UID. Together they ensure the process runs as a non-root user that also owns workspace files correctly on the host. Drop remoteUser and the tooling may fall back to a different account than the one you meant to keep-id, so the running process and the mapped identity diverge. Keeping both explicit in devcontainer.json removes that ambiguity and documents intent for anyone else who opens the project.

Does keep-id work the same on a multi-user host? The mechanism is identical, but each user needs their own non-overlapping range in /etc/subuid and /etc/subgid. keep-id maps to whichever host user is running Podman, so on a shared machine two developers each get their own files owned by their own account, provided their subordinate ranges do not collide. If ranges overlap, the mappings can point at the same host IDs and ownership becomes ambiguous, so allocate ranges per user before relying on keep-id.

Can I use keep-id with a rootful Podman or Docker daemon? No — it is a rootless construct. A rootful daemon runs containers as real root and writes files as root or whatever UID the process assumes, with no subordinate mapping in play, so --userns=keep-id is unnecessary and unsupported there. If you migrate a rootful setup to rootless and suddenly see foreign ownership, keep-id is the missing piece rather than a bug in the migration.

What if stat shows my user but Git still complains about ownership? That is usually a separate safe-directory check, not a mapping failure. Recent Git refuses to operate on a repository owned by a different user than the caller, and even with correct keep-id ownership a stray root-owned file from an earlier run can trip it. Confirm every file reports your user with a recursive stat or ls -n, remove any leftovers created before the mapping was in place, and add a safe.directory entry only as a last resort.