Docker 57 🐳 Rootless Docker Architecture: Running Docker Engine without Root Privileges
Running Docker as root has always been a security trade-off. The daemon runs with full privileges, and a container escape can escalate to host root. Rootless mode changes that. The daemon and every container it runs execute as an unprivileged user, using user namespaces and subordinate UID mappings to create the illusion of root inside the container while the host sees an ordinary user. A breakout yields nothing more than the unprivileged user’s own permissions. The cost is a set of architectural constraints: a userspace network stack, a limited set of storage drivers, and a cgroup delegation requirement that ties rootless mode to systemd and cgroup v2.
Key point: Rootless Docker does not run Docker “without root” in the sense of removing the daemon’s privileges entirely. It runs the daemon inside a user namespace, where it has root-like capabilities that apply only within that namespace. The host sees an unprivileged process, and the kernel enforces the boundary through newuidmap and newgidmap.
Why rootless Docker exists
The daemon-as-root problem. The Docker daemon runs as root by default because it needs to create network interfaces, mount filesystems, and manage cgroups. An attacker who exploits the daemon, or escapes a container, inherits those privileges. Rootless mode removes that inheritance.
The user namespace mechanism. Linux user namespaces let an unprivileged process create a namespace where it appears to be root. The newuidmap and newgidmap tools, along with the /etc/subuid and /etc/subgid files, map the namespace’s root UID to a high-numbered UID on the host . The kernel enforces the mapping, so operations that require root inside the namespace fail outside it.
The RootlessKit layer. RootlessKit is the tool that sets up the user namespace and the mount namespace, and optionally the network namespace . Docker’s rootless mode uses RootlessKit to create the namespace, then runs dockerd inside it. The dockerd-rootless.sh script is the wrapper that configures the environment and invokes RootlessKit .
The port-binding problem. A non-root user cannot bind ports below 1024. A rootless daemon that wants to publish port 80 on the host must either use CAP_NET_BIND_SERVICE on the rootlesskit binary, or set net.ipv4.ip_unprivileged_port_start=0, or bind to a high port and use a reverse proxy .
The networking problem. Rootless containers cannot use the kernel bridge that rootful Docker uses. The docker0 interface does not exist on the host, and container IPs are not directly reachable . The network stack is a userspace NAT, typically slirp4netns, with pasta as an experimental alternative on Docker Engine 25.0+ .
The cgroup problem. Resource limits like --cpus and --memory require cgroup delegation, which rootless mode supports only on cgroup v2 with systemd . The cgroupfs driver is not supported because there is no easy way to delegate cgroup v2 without systemd .
The storage problem. The OverlayFS snapshotter does not work inside user namespaces except on Ubuntu and Debian kernels. The fuse-overlayfs snapshotter is the alternative for kernels >= 4.18 .
a. The prerequisites and installation
Rootless mode requires three things: the uidmap package, subordinate UID/GID ranges, and a kernel that supports unprivileged user namespaces.
The prerequisites
The uidmap package provides newuidmap and newgidmap. The /etc/subuid and /etc/subgid files must contain at least 65,536 subordinate IDs for the user .
# Install the uidmap package
sudo apt install -y uidmap
# Check the subordinate ranges
grep ^$(whoami): /etc/subuid
# testuser:231072:65536
If the ranges are missing, they must be added. The usermod command with --add-subuids and --add-subgids is the standard way.
The installation script
The dockerd-rootless-setuptool.sh script is the official installer. It is included in the docker-ce-rootless-extras package on RPM and DEB systems, or can be fetched from https://get.docker.com/rootless .
# Run the setup script as a non-root user
dockerd-rootless-setuptool.sh install
# If the script is not present, install the extras package
sudo apt-get install -y docker-ce-rootless-extras
The script creates the systemd user unit, installs the rootless CLI context, and prints the environment variables that must be set .
The systemd user unit
The daemon runs as a systemd user service, not a system service. It is managed with systemctl --user .
# Start the daemon
systemctl --user start docker
# Enable it on login
systemctl --user enable docker
# Enable lingering so the service starts on boot
sudo loginctl enable-linger $(whoami)
Lingering is required for the service to start without an active login session .
The CLI context
Docker Engine v23 and later automatically configure the CLI to use the rootless context when it is available. The docker info command confirms the connection with a rootless security option .
docker info | grep -i rootless
# rootless
For older versions, the context must be set explicitly with docker context use rootless or the DOCKER_HOST environment variable .
The data directories
The daemon’s data directory is ~/.local/share/docker by default, the config directory is ~/.config/docker, and the socket is at $XDG_RUNTIME_DIR/docker.sock . The XDG_RUNTIME_DIR must be set and writable, and it should not be under /tmp because of TOCTOU vulnerabilities .
b. The network architecture
Rootless networking is the most visible difference from rootful mode. The kernel bridge that rootful Docker uses is not available, so the network stack runs in userspace.
The slirp4netns stack
slirp4netns is the default network driver. It creates a userspace NAT that connects the container’s network namespace to the host’s network stack .
# The default driver
DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns
The container gets a private IP from the Docker-managed network, but the bridge interface lives inside the rootless daemon’s network namespace, not the host’s . The docker0 interface does not appear in ip link show on the host .
The pasta alternative
pasta (Pack A Subtle Tap Abstraction) is an experimental alternative to slirp4netns on Docker Engine 25.0+. It uses the RootlessKit port driver with an implicit port mapping .
# Install pasta
sudo apt install passt
# Configure the daemon to use pasta
mkdir -p ~/.config/systemd/user/docker.service.d
tee ~/.config/systemd/user/docker.service.d/override.conf << 'EOF'
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"
EOF
systemctl --user daemon-reload
systemctl --user restart docker
The port publishing
Port publishing works through a forwarder that listens on the host and forwards connections to the container. The RootlessKit builtin port driver is the default. Before RootlessKit v3.0, it did not preserve the original source IP address; slirp4netns and pasta were the alternatives for access logging . RootlessKit v3.0, bundled with Docker v29.5, added source IP propagation, which requires disabling the userland proxy .
# Disable the userland proxy for source IP propagation
mkdir -p ~/.config/docker
echo '{"userland-proxy": false}' >~/.config/docker/daemon.json
systemctl --user restart docker
The host access
From inside a rootless container, the host is reachable at 10.0.2.2 when using slirp4netns. The address is not localhost or 127.0.0.1, because those refer to the container’s own network namespace .
# From inside the container
curl http://10.0.2.2:8080/
The custom networks
Custom bridge networks work in rootless mode, but the bridge exists inside the daemon’s network namespace. Container-to-container communication works within the network, and host access requires published ports .
# Create a custom network
docker network create --subnet 192.168.200.0/24 mynet
# Run containers on it
docker run -d --network mynet --name web nginx:alpine
docker run -d --network mynet --name db postgres:15-alpine
The privileged ports
Publishing host ports below 1024 requires either CAP_NET_BIND_SERVICE on the rootlesskit binary or a sysctl change.
# Allow rootless Docker to publish privileged host ports
echo "net.ipv4.ip_unprivileged_port_start=0" | sudo tee -a /etc/sysctl.d/99-rootless.conf
sudo sysctl --system
# Now port 80 can be published
docker run -d -p 80:80 nginx
The sysctl change allows all users to bind to any port, which is broader than granting the capability to a single binary .
c. The storage and cgroup constraints
Rootless mode’s storage and resource-management capabilities depend on the kernel version and the cgroup mode.
The storage drivers
The OverlayFS snapshotter does not work inside user namespaces on most kernels. The exception is Ubuntu and Debian kernels, which have a patch that enables it . On other distributions, the fuse-overlayfs snapshotter is the alternative for kernels >= 4.18 .
| Driver | Rootless Support |
|---|---|
| overlayfs | Ubuntu/Debian kernels only |
| fuse-overlayfs | Kernel >= 4.18 |
| native | Works, but copies layers |
| vfs | Works, but no layering |
The native and vfs drivers work without special kernels, but they consume more disk space because they do not share layers.
The cgroup delegation
Resource limits (--cpus, --memory, --pids-limit) require cgroup delegation. Rootless mode supports this only when the host runs cgroup v2 with systemd .
# Check the cgroup version
stat -fc %T /sys/fs/cgroup/
# cgroup2fs (v2) or tmpfs (v1)
The cgroupfs driver is not supported because delegating cgroup v2 without systemd requires BPF, which the rootless setup cannot always perform .
The kind integration
kind supports rootless Docker as a node provider starting with kind 0.11.0 and Docker 20.10 . The host must be on cgroup v2, and the same restrictions apply: OverlayFS limitations, no block storage mounts, and no NFS mounts .
The Docker-in-Docker variant
The docker:dind-rootless image runs the daemon as a non-root user inside a container. It requires the --privileged flag to disable seccomp, AppArmor, and mount masks . The cgroup driver inside the image must be systemd, not cgroupfs .
Complete Example Session
# ============================================
# PART 1: THE PREREQUISITES
# ============================================
# Install the uidmap package
sudo apt install -y uidmap
# Verify the subordinate ranges
grep ^$(whoami): /etc/subuid
# testuser:231072:65536
grep ^$(whoami): /etc/subgid
# testuser:231072:65536
# ============================================
# PART 2: THE INSTALLATION
# ============================================
# Run the setup script as a non-root user
dockerd-rootless-setuptool.sh install
# Output:
# [INFO] Creating /home/testuser/.config/systemd/user/docker.service
# [INFO] Installed docker.service successfully.
# [INFO] To control docker.service, run: systemctl --user (start|stop|restart) docker.service
# [INFO] To run docker.service on system startup, run: sudo loginctl enable-linger testuser
# ============================================
# PART 3: STARTING THE DAEMON
# ============================================
# Start the service
systemctl --user start docker
# Enable lingering
sudo loginctl enable-linger $(whoami)
# ============================================
# PART 4: VERIFYING THE CONNECTION
# ============================================
docker info | grep -i rootless
# rootless
# ============================================
# PART 5: THE NETWORK BEHAVIOR
# ============================================
# The docker0 interface is not on the host
ip link show | grep docker0
# (no output)
# Containers get private IPs
docker run -d --name netdemo alpine sleep 60
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' netdemo
# 172.17.0.2
# ============================================
# PART 6: THE HOST ACCESS FROM A CONTAINER
# ============================================
docker exec netdemo wget -qO- http://10.0.2.2:8080/
# (the host's service response)
# ============================================
# PART 7: THE PRIVILEGED PORT
# ============================================
# Without the sysctl, publishing port 80 fails
docker run -d -p 80:80 nginx
# Error: rootlesskit: bind: permission denied
# With the sysctl
echo "net.ipv4.ip_unprivileged_port_start=0" | sudo tee -a /etc/sysctl.d/99-rootless.conf
sudo sysctl --system
docker run -d -p 80:80 nginx
# (succeeds)
# ============================================
# PART 8: THE PASTA ALTERNATIVE
# ============================================
sudo apt install passt
mkdir -p ~/.config/systemd/user/docker.service.d
tee ~/.config/systemd/user/docker.service.d/override.conf << 'EOF'
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"
EOF
systemctl --user daemon-reload
systemctl --user restart docker
# ============================================
# PART 9: THE CGROUP CHECK
# ============================================
stat -fc %T /sys/fs/cgroup/
# cgroup2fs
# Resource limits work
docker run --rm --memory=512m alpine free -m
# (shows the limit)
# ============================================
# PART 10: THE CLEANUP
# ============================================
docker rm -f netdemo
dockerd-rootless-setuptool.sh uninstall
The ten parts showed the prerequisites, the installation, starting the daemon, verifying the connection, the network behavior, the host access, the privileged port, the pasta alternative, the cgroup check, and the cleanup.
Quick Reference
The Prerequisites
| Requirement | Purpose |
|---|---|
uidmap package | Provides newuidmap and newgidmap |
/etc/subuid | 65,536+ subordinate UIDs |
/etc/subgid | 65,536+ subordinate GIDs |
XDG_RUNTIME_DIR | The socket directory |
slirp4netns or pasta | The network stack |
The Commands
| Command | Purpose |
|---|---|
dockerd-rootless-setuptool.sh install | Install the daemon |
dockerd-rootless-setuptool.sh uninstall | Remove the daemon |
systemctl --user start docker | Start the daemon |
systemctl --user enable docker | Enable on login |
loginctl enable-linger | Start on boot |
docker context use rootless | Use the rootless context |
The Network Drivers
| Driver | Status | Source IP |
|---|---|---|
slirp4netns | Default | Yes (with port driver) |
pasta | Experimental (25.0+) | Yes |
vpnkit | Fallback | No |
gvisor-tap-vsock | Alternative | No |
The Storage Drivers
| Driver | Rootless | Notes |
|---|---|---|
overlayfs | Ubuntu/Debian only | Kernel patch |
fuse-overlayfs | Kernel >= 4.18 | FUSE-based |
native | Yes | Copies layers |
vfs | Yes | No layering |
The Limitations
| Feature | Rootful | Rootless |
|---|---|---|
| Ports < 1024 | Yes | Sysctl or capability |
macvlan | Yes | No |
ipvlan | Yes | No |
| Host network | Yes | Limited |
| Resource limits | cgroup v1/v2 | cgroup v2 + systemd |
| Block storage mounts | Yes | No |
| NFS mounts | Yes | No |
Best Practices
✅ Do This:
# Install the uidmap package before setup.
sudo apt install -y uidmap # ✅
# Verify the subordinate ranges.
grep ^$(whoami): /etc/subuid # ✅
# Run the setup as a non-root user.
dockerd-rootless-setuptool.sh install # ✅
# Enable lingering for boot persistence.
sudo loginctl enable-linger $(whoami) # ✅
# Use pasta for better performance on Docker 25.0+.
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta" # ✅
# Use fuse-overlayfs on kernels without the overlay patch.
# Check the kernel version first. # ✅
# Disable the userland proxy for source IP propagation.
echo '{"userland-proxy": false}' > ~/.config/docker/daemon.json # ✅
❌ Don’t Do This:
# Don't run the setup as root.
sudo dockerd-rootless-setuptool.sh install # ❌ Must be non-root
# Don't use the cgroupfs driver.
# Rootless requires systemd and cgroup v2. # ❌
# Don't expect docker0 on the host.
ip link show | grep docker0 # ❌ It is in the namespace
# Don't use macvlan or ipvlan.
docker network create -d macvlan ... # ❌ Requires root
# Don't use XDG_RUNTIME_DIR under /tmp.
# TOCTOU vulnerability. # ❌
# Don't forget the sysctl for privileged ports.
docker run -p 80:80 nginx # ❌ Permission denied
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Setup fails | uidmap missing | Install the package |
| Setup fails | No subordinate ranges | Add them to /etc/subuid |
| Daemon not starting | XDG_RUNTIME_DIR unset | Set it in .bashrc |
| Port 80 fails | No capability or sysctl | Add the sysctl |
docker0 missing | Network namespace | Expected; use published ports |
| Resource limits ignored | cgroup v1 | Enable cgroup v2 + systemd |
| OverlayFS fails | Kernel without patch | Use fuse-overlayfs |
docker not found | PATH missing ~/bin | Export the path |
Real-World Examples
1. Install Rootless Docker
dockerd-rootless-setuptool.sh install
2. Start the Daemon
systemctl --user start docker
3. Enable Lingering
sudo loginctl enable-linger $(whoami)
4. Verify Rootless
docker info | grep -i rootless
5. Publish a Privileged Port
echo "net.ipv4.ip_unprivileged_port_start=0" | sudo tee -a /etc/sysctl.d/99-rootless.conf
sudo sysctl --system
docker run -d -p 80:80 nginx
6. Use Pasta
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta"
7. Custom Network
docker network create --subnet 192.168.200.0/24 mynet
8. Host Access from Container
curl http://10.0.2.2:8080/
9. Disable Userland Proxy
echo '{"userland-proxy": false}' > ~/.config/docker/daemon.json
10. Check Cgroup Version
stat -fc %T /sys/fs/cgroup/
Visual
The Rootless Architecture
┌────────────────────────────────────────────────────────────┐
│ ROOTLESS DOCKER ARCHITECTURE │
│ │
│ HOST (unprivileged user) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RootlessKit │ │
│ │ - Creates user namespace │ │
│ │ - Creates mount namespace │ │
│ │ - Creates network namespace │ │
│ │ - Runs slirp4netns or pasta │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ Executes │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ USER NAMESPACE │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ dockerd (appears as root) │ │ │
│ │ │ - UID 0 inside the namespace │ │ │
│ │ │ - UID 231072 on the host │ │ │
│ │ │ │ │ │
│ │ │ Containers │ │ │
│ │ │ - UID 0 inside the container │ │ │
│ │ │ - UID 231073+ on the host │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ A container escape yields UID 231072 on the host. │
│ No root privileges outside the namespace. │
└────────────────────────────────────────────────────────────┘
The Network Stack
┌────────────────────────────────────────────────────────────┐
│ ROOTLESS NETWORKING │
│ │
│ HOST NETWORK NAMESPACE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ eth0 (host) │ │
│ │ │ │
│ │ Published port: 8080 → 172.17.0.2:80 │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ RootlessKit port forwarder │
│ v │
│ ROOTLESS DAEMON NETWORK NAMESPACE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker0 (inside the namespace) │ │
│ │ 172.17.0.1 │ │
│ │ │ │
│ │ Container: 172.17.0.2 │ │
│ │ │ │
│ │ slirp4netns / pasta │ │
│ │ - Userspace NAT │ │
│ │ - No kernel bridge on the host │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The docker0 interface is not visible on the host. │
│ Container IPs are not directly reachable from the host. │
└────────────────────────────────────────────────────────────┘
The Port Binding
┌────────────────────────────────────────────────────────────┐
│ PRIVILEGED PORT BINDING │
│ │
│ WITHOUT TUNING │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker run -p 80:80 nginx │ │
│ │ → rootlesskit: bind: permission denied │ │
│ │ │ │
│ │ The user cannot bind port 80. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITH SYSCTL │
│ ┌──────────────────────────────────────────────────┐ │
│ │ net.ipv4.ip_unprivileged_port_start=0 │ │
│ │ │ │
│ │ docker run -p 80:80 nginx │ │
│ │ → succeeds │ │
│ │ │ │
│ │ All users can now bind to any port. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITH CAPABILITY │
│ ┌──────────────────────────────────────────────────┐ │
│ │ setcap cap_net_bind_service=ep $(which rootlesskit)│ │
│ │ │ │
│ │ Only the rootlesskit binary gains the capability.│ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The cgroup Requirement
┌────────────────────────────────────────────────────────────┐
│ CGROUP V2 AND SYSTEMD │
│ │
│ ROOTLESS RESOURCE LIMITS REQUIRE: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. cgroup v2 (unified hierarchy) │ │
│ │ 2. systemd │ │
│ │ 3. Docker 20.10+ │ │
│ │ 4. containerd 1.4+ │ │
│ │ 5. runc 1.0-rc91+ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITHOUT: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker run --memory=512m alpine │ │
│ │ → the limit is ignored │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Check the cgroup version: │
│ stat -fc %T /sys/fs/cgroup/ │
│ cgroup2fs → v2 │
│ tmpfs → v1 │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Rootless mode | Docker daemon and containers as a non-root user |
| RootlessKit | The tool that creates the user namespace |
newuidmap / newgidmap | The UID/GID mapping tools |
/etc/subuid / /etc/subgid | The subordinate ID ranges |
dockerd-rootless-setuptool.sh | The installer |
systemctl --user | The daemon management |
slirp4netns | The default network stack |
pasta | The experimental alternative |
fuse-overlayfs | The storage driver for most kernels |
| cgroup v2 + systemd | Required for resource limits |
Key takeaways:
- Rootless mode runs the daemon inside a user namespace. The daemon appears as root inside the namespace, but the host sees an unprivileged UID. The
newuidmapandnewgidmaptools enforce the mapping, and the/etc/subuidand/etc/subgidfiles provide the subordinate ranges . - The installation requires the
uidmappackage and subordinate ranges. Thedockerd-rootless-setuptool.shscript creates the systemd user unit and the CLI context. It runs as a non-root user and does not requiresudo. - The daemon runs as a systemd user service. It is managed with
systemctl --user, and lingering must be enabled for it to start on boot. Running it as a system-wide service is not supported . - The network stack is userspace NAT. The
slirp4netnsdriver is the default, andpastais the experimental alternative on Docker 25.0+. Thedocker0interface does not exist on the host, and container IPs are not directly reachable . - Privileged ports require tuning. Publishing ports below 1024 requires either the
CAP_NET_BIND_SERVICEcapability on therootlesskitbinary or thenet.ipv4.ip_unprivileged_port_start=0sysctl . - The storage driver is constrained. OverlayFS works only on Ubuntu and Debian kernels. The
fuse-overlayfssnapshotter is the alternative for kernels >= 4.18 . - Resource limits require cgroup v2 and systemd. The cgroupfs driver is not supported for rootless mode. The
--cpusand--memoryflags work only when the host runs cgroup v2 with systemd . - The
10.0.2.2address reaches the host. From inside aslirp4netnscontainer, the host is at10.0.2.2, notlocalhostor127.0.0.1.
Remember: Rootless Docker runs the daemon and its containers as an unprivileged user, using user namespaces and subordinate ID mappings to create the illusion of root inside the namespace. The host sees an ordinary user, and a container escape yields nothing more than that user’s permissions. The architecture relies on RootlessKit, slirp4netns or pasta for networking, and fuse-overlayfs for storage on most kernels. The constraints are real: no docker0 on the host, no privileged ports without tuning, no macvlan or ipvlan, and no resource limits without cgroup v2 and systemd. The result is a Docker daemon that is safe to run on a shared machine, a developer laptop, or a CI runner, where root access is not available and not wanted.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!