What is rootless Docker and when to use it?
Rootless Docker runs the entire Docker stack as an unprivileged user. The daemon does not need root; neither do the containers. It is the strongest security hardening you can apply to a Docker installation, paid for with some operational trade-offs.
Theory
TL;DR
- Standard Docker:
dockerdruns as root. Container processes are constrained but the daemon itself is privileged. - Rootless Docker:
dockerdruns as a regular user via Linux user namespaces. Whole stack is unprivileged on the host. - A container escape from rootless Docker = compromise of the user, not host root. Big difference for shared infrastructure.
- Trade-offs: no
--privilegedcontainers, no port binding below 1024 by default, slower network via slirp4netns, ~20% storage I/O overhead, nodocker run --network hostfor the host. - Use when: security-sensitive multi-tenant hosts, CI runners, HPC clusters, regulated environments. Skip when: need privileged containers, very high I/O workloads, kernel modules.
How it works
Rootless Docker uses user namespaces to remap UIDs:
Host: Inside rootless container:
user 'me' (UID 1000) root (UID 0)
↑ ↓
+- runs dockerd +- inside this user namespace,
which uses subuids 100000-165535 'root' is a local construct
↓ that maps to host UID 100000
+- containers run as +- escape gives access to
UIDs 100000-165535 UID 100000, NOT host rootThe kernel's user namespace gives each container its own UID 0 (root inside) that maps to a different, unprivileged UID outside. Daemon runs as the regular user (no UID 0 needed on host).
Installing rootless Docker
# Install (does not need sudo for the rootless install itself)
curl -fsSL https://get.docker.com/rootless | sh
# The installer prints instructions; key parts:
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
# Start the daemon
systemctl --user start docker
# (or `dockerd-rootless.sh &` directly)
docker run --rm hello-worldThe DOCKER_HOST env var points to the user's daemon socket, separate from any system Docker.
Trade-offs
What rootless gives up
# Cannot bind to low ports without setcap (system-wide)
docker run -p 80:80 nginx # "permission denied" by default
# Workaround: use a higher port (8080), or grant CAP_NET_BIND_SERVICE
# No --privileged or capability additions beyond user limits
docker run --privileged ... # fails
# Slower network (slirp4netns userland TCP/IP stack)
# Throughput: ~half of bridge networking
# Slower storage (overlay2 needs special setup; often falls back to fuse-overlayfs)
# I/O: ~20% slower than rootful
# No system services / no host network
docker run --network host ... # failsMost user-space workloads (web servers on high ports, app containers, language runtimes) work fine. Privileged or kernel-level work breaks.
What rootless gains
- No root daemon to attack. The single biggest privilege-escalation surface in classical Docker is gone.
- Per-user isolation. Two users on the same host run separate Docker stacks; one cannot see the other's containers.
- No
dockergroup risk. The classical "add me to docker group" = root warning does not apply; the rootless daemon is bound to the user. - Easier audit story. "What can this Docker do?" answer: "whatever its user can."
Comparison with Podman
Podman is daemonless and rootless by default. The two solutions overlap:
| Rootless Docker | Podman | |
|---|---|---|
| Daemon | yes (per-user dockerd) | no (each podman is its own process) |
| Rootless | yes (opt-in install) | yes (default) |
| Compose support | yes (docker compose) | yes (podman compose) |
| Image format | OCI | OCI |
| Maturity | mature | mature |
| Linux distro defaults | Docker is the de-facto standard | Podman is default on RHEL/Fedora |
For security-first deployments on Red Hat-based systems, Podman is the path of least resistance. For mixed Docker/legacy compatibility, rootless Docker.
Setting up port bindings below 1024
# Grant the rootlesskit binary capability to bind low ports
sudo setcap cap_net_bind_service=ep $(which rootlesskit)
# Or per-container, less commonThis is the most common operational hurdle. Once set up, -p 80:80 works.
Common mistakes
Mixing rootless and rootful daemons
# Both could be running
sudo systemctl status docker # system-wide rootful
systemctl --user status docker # user-level rootlessIf both are running, docker commands go to whichever DOCKER_HOST points at. Confusing. Pick one per machine.
Trying --privileged and getting unhelpful errors
$ docker run --privileged ubuntu mount
mount: /proc/sys: must be mounted on /proc/sysRootless cannot grant --privileged. If your app needs it, rootless is not for you.
Forgetting that the network is slower
For most apps, slirp4netns is fine (typical web traffic). For high-throughput services, benchmark before committing.
Filesystem permission surprises
$ docker run -v /home/me/data:/data myapp
# Inside: files appear owned by 'nobody' or weird UIDsThe rootless daemon's UID-mapping affects file ownership. -v bind mounts may show unfamiliar ownership inside the container. Plan UIDs accordingly.
Real-world usage
- CI runners: GitHub Actions self-hosted runners often use rootless Docker — multiple users on one host, no escape to host root.
- HPC clusters: scientific computing on shared nodes; rootless gives each user their own Docker without sysadmin grant.
- Per-tenant container hosts: multi-tenant servers where each tenant runs their own dockerd unprivileged.
- Regulated environments: finance, defense, healthcare — auditors love "no root daemon".
- Developer laptops on cooperative machines: if the laptop is shared (rare these days but exists), rootless prevents one user's container from owning the host.
Follow-up questions
Q: Does rootless Docker work on macOS / Windows?
A: Docker Desktop on Mac/Windows already runs the daemon inside a Linux VM, isolating it from your host OS. Functionally similar security to rootless on Linux. The rootless terminology is for Linux-host scenarios.
Q: Is rootless Docker production-ready?
A: Yes, since around 2020. Used at scale by GitHub, Red Hat (via Podman), various HPC sites. Mature enough to be the default for security-conscious deployments.
Q: What is slirp4netns?
A: A userspace TCP/IP stack that gives unprivileged containers network connectivity. Slower than kernel networking (because it implements TCP/IP in userspace), but does not require root. There is also vpnkit (used by Docker Desktop) and pasta (newer, faster).
Q: Can I run rootless Docker alongside rootful Docker on the same host?
A: Yes, but they are separate. Different sockets (/var/run/docker.sock vs $XDG_RUNTIME_DIR/docker.sock), different containers, different networks. DOCKER_HOST chooses which one your CLI talks to.
Q: (Senior) When does the rootless trade-off NOT make sense?
A: When your workload demands kernel features rootless cannot provide: device pass-through, kernel module loading, low-level network manipulation (raw sockets, eBPF), real --privileged for nested virtualization, or extremely I/O-bound workloads where the 20% overhead is unacceptable. For these, accept rootful Docker and harden it via other means (seccomp, AppArmor, user namespace remapping at the daemon level via userns-remap).
Examples
Side-by-side: same docker run, different security model
# Rootful Docker (default)
sudo systemctl start docker
docker run --rm alpine ps -ef
# UID 0 inside, dockerd as root on host, container escape = host root
# Rootless Docker
systemctl --user start docker
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker run --rm alpine ps -ef
# UID 0 inside, dockerd as user on host, container escape = unprivileged userSame command, very different blast radius.
CI runner setup
# As the runner user
curl -fsSL https://get.docker.com/rootless | sh
# Persist env
cat >> ~/.bashrc <<'EOF'
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
EOF
# Auto-start on login (lingering keeps service running between SSH sessions)
sudo loginctl enable-linger runner
systemctl --user enable docker
systemctl --user start dockerNow each runner instance has its own Docker stack, no shared root daemon.
Verifying rootless
$ docker info | grep -i 'rootless\|cgroup'
rootless
Cgroup Driver: cgroupfs
$ ps -ef | grep dockerd
runner 12345 1 /home/runner/bin/dockerd-rootless.sh
# Note: NOT running as root
$ docker run --rm alpine cat /proc/self/status | grep CapEff
CapEff: 0000003fffffffff
# Capabilities still constrained to user's allowanceThe daemon process belongs to runner, not root. Container capabilities are bounded by the user's host capabilities.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet