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 infosucceeding as your user. /etc/subuidand/etc/subgidcontaining 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.
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.
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.
{
"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.
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.
# 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.
| Symptom | Root Cause | Remediation |
|---|---|---|
| Workspace files owned by a strange UID | No keep-id userns mapping | Add --userns=keep-id to runArgs |
| Cannot bind port 80/443 | Rootless can't bind low ports | Use a port above 1024, or configure the range |
podman info fails | Rootless socket not running | Enable the user podman socket service |
| Slow builds vs Docker | fuse-overlayfs overhead | Use native rootless overlayfs if supported |
| Compose services don't start | Used docker compose, not podman-compose | Use 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.
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.
Related
- DevContainer Architecture & Core Tooling — the engine-agnostic model Podman plugs into.
- Mapping UIDs for Rootless Podman DevContainers — the userns/keep-id details.
- Using podman-compose in a DevContainer — multi-service stacks without Docker Compose.
- Migrating from Docker to Podman in DevContainers — switching engines cleanly.
- devcontainer.json Property Reference — the
runArgs,remoteUser, and UID keys involved.