Podman & Rootless Container Engines

Docker Desktop's licensing and its root daemon push many teams toward Podman and rootless engines — and the DevContainer spec supports them, because it only needs a Docker-compatible engine, not Docker specifically. This guide covers running devcontainers on rootless Podman: the user-namespace model that keeps container processes off root, the keep-id mapping that keeps your files yours, and the perf and port caveats that come with rootless. It complements the engine-agnostic setup in the architecture overview.

The central concept is the user namespace. A rootless engine runs entirely as your unprivileged host user; inside the container, processes can be "root," but that root is mapped — through your subuid/subgid ranges — back to your host user. Understanding that mapping is what makes rootless devcontainers behave predictably.

Prerequisites

You need Podman installed with its socket enabled, subuid/subgid ranges configured for your user, the Dev Container CLI (or editor) pointed at podman, and an understanding of the keep-id userns option.

  • Podman installed and podman info succeeding as your user.
  • /etc/subuid and /etc/subgid containing a range for your user.
  • dev.containers.dockerPath (editor) or the CLI configured to use podman.
  • Awareness that ports below 1024 need extra configuration when rootless.

Rootless prerequisitesYou need Podman with a socket, subuid/subgid mappings, the CLI pointed at podman, and userns config.Podmaninstalled + socketRootlesssubuid/subgid mappeddevcontainer CLIdockerPath=podmanConfiguserns keep-id

Architecture & Configuration Deep Dive

Rootless works by nesting namespaces. Your host user owns a range of subordinate UIDs (in /etc/subuid); when a rootless container starts, its internal UID 0 (root) maps to your host UID, and its other UIDs map into your subordinate range. This means a process running as root inside the container is, from the host's view, just you — it cannot touch anything you can't. The trade-off is that file ownership on bind mounts needs care, which is exactly what the keep-id mapping solves.

Rootless UID modelA rootless engine maps container users into your host user's namespace via subuid/subgid ranges.Rootless engineruns as your user, no daemon rootUser namespacecontainer root maps to host usersubuid/subgidrange of sub-IDs for other userskeep-id mappingaligns container UID with host

For a devcontainer, the important consequence is bind-mount ownership. Without keep-id, files a container writes to your workspace can appear owned by a subordinate UID rather than your user. Passing --userns=keep-id (via runArgs) maps the container user directly to your host UID, so files stay yours. The UID-mapping mechanics carry over from the permission discipline in the property reference's remoteUser/updateRemoteUserUID section — rootless just adds the namespace layer.

Step-by-Step Implementation

Point the tooling at Podman, set the userns mapping so ownership is correct, then build and attach.

Podman wiringPoint the CLI at podman, set userns keep-id, build, and attach with correct file ownership.Point CLIdockerPath=podmanusernskeep-id in runArgsBuildpodman builds imageAttachfiles stay your-owned

{
  "name": "Rootless Podman",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu@sha256:PINNED",
  "runArgs": ["--userns=keep-id"],
  "remoteUser": "vscode"
}
# Editor: point the Dev Containers extension at podman
#   "dev.containers.dockerPath": "podman"
# CLI: build headlessly with podman as the engine
DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock" devcontainer up --workspace-folder .

Rootless UID mapping in depth is covered in mapping UIDs for rootless Podman devcontainers, multi-service stacks in using podman-compose in a devcontainer, and switching engines in migrating from Docker to Podman in devcontainers.

Performance & Resource Optimization

Rootless engines run at near-parity with rootful Docker; the main performance variable is the storage driver. Native overlayfs (available rootless on recent kernels) is fastest; fuse-overlayfs is the portable fallback with a modest overhead. Build caching and named volumes work the same as with Docker.

Relative build throughputRootless engines run at near-parity; the storage driver is the main variable.Rootful DockerbaselineRootless Podman~96%Rootless + fuse-overlayfs~88%illustrative relative throughput

To keep rootless fast, prefer native rootless overlayfs where your kernel supports it, and cache package managers on named volumes exactly as you would under Docker — the registry pull-caching and per-language cache strategies are engine-agnostic. The one place to budget extra time is the first build, where the userns and storage setup add a small overhead.

Validation & Testing

Validate two things: the engine is reachable by the tooling, and workspace files created inside the container are owned by your host user (proving keep-id works). The CLI drives this headlessly, so it works in rootless CI too.

Rootless validationConfirm the engine is reachable and workspace files stay owned by your host user.Does podman info reach the engine?NOFix the rootless socket pathAre workspace files owned by your user?YESuserns keep-id is workingRootless devcontainer is correct

# Prove ownership is correct after a rootless build
devcontainer exec --workspace-folder . -- sh -c 'touch /workspace/.owncheck'
stat -c '%U' .owncheck   # should be your host username, not a subuid

Common Pitfalls

The failures below are almost all UID-mapping or low-port issues unique to rootless. The triage below sorts them.

Rootless pitfall triageA triage path from ownership or port issues to a clean rootless setup.Are workspace files root-owned on thehost?YESAdd userns=keep-idCan't bind a low port?YESUse a port above 1024Rootless works cleanly

SymptomRoot CauseRemediation
Workspace files owned by a strange UIDNo keep-id userns mappingAdd --userns=keep-id to runArgs
Cannot bind port 80/443Rootless can't bind low portsUse a port above 1024, or configure the range
podman info failsRootless socket not runningEnable the user podman socket service
Slow builds vs Dockerfuse-overlayfs overheadUse native rootless overlayfs if supported
Compose services don't startUsed docker compose, not podman-composeUse podman-compose or the compose provider

Conclusion

Rootless Podman gives you devcontainers with no root daemon and a smaller attack surface, at the cost of understanding one thing well: the user namespace. Map the container user to your host user with keep-id so bind-mounted files stay yours, use native rootless storage for speed, and keep ports above 1024. Get those right and a rootless devcontainer is indistinguishable from a Docker one — just safer.

Rootless trade-offsRootless removes the root daemon at the cost of UID-mapping and low-port nuances.Rootless winsNo daemon running as rootSmaller attack surfacePer-user isolationWatch forUID mapping quirksStorage-driver perfPort <1024 limits

FAQ

Do devcontainers work without Docker at all? Yes. The spec requires a Docker-compatible engine, which Podman provides. Point the Dev Containers extension or CLI at podman (and podman-compose for multi-service stacks), and the same .devcontainer/ builds and attaches. Rootless adds a user-namespace layer, but the configuration is otherwise unchanged.

Why are my workspace files owned by a weird user ID after a rootless build? Because rootless maps container UIDs into your subordinate ID range, so a file written by container root lands as a subordinate UID on the host. Add --userns=keep-id to runArgs so the container user maps directly to your host UID, and files created in the workspace stay owned by you.

Can I bind low ports (80, 443) in a rootless devcontainer? Not by default — rootless engines can't bind ports below 1024 without extra privileges. Forward a higher port (for example 8080) instead, or configure net.ipv4.ip_unprivileged_port_start on the host if you genuinely need a low port. For most dev servers, a high port plus forwardPorts is simplest.