| |

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 .

DriverRootless Support
overlayfsUbuntu/Debian kernels only
fuse-overlayfsKernel >= 4.18
nativeWorks, but copies layers
vfsWorks, 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

RequirementPurpose
uidmap packageProvides newuidmap and newgidmap
/etc/subuid65,536+ subordinate UIDs
/etc/subgid65,536+ subordinate GIDs
XDG_RUNTIME_DIRThe socket directory
slirp4netns or pastaThe network stack

The Commands

CommandPurpose
dockerd-rootless-setuptool.sh installInstall the daemon
dockerd-rootless-setuptool.sh uninstallRemove the daemon
systemctl --user start dockerStart the daemon
systemctl --user enable dockerEnable on login
loginctl enable-lingerStart on boot
docker context use rootlessUse the rootless context

The Network Drivers

DriverStatusSource IP
slirp4netnsDefaultYes (with port driver)
pastaExperimental (25.0+)Yes
vpnkitFallbackNo
gvisor-tap-vsockAlternativeNo

The Storage Drivers

DriverRootlessNotes
overlayfsUbuntu/Debian onlyKernel patch
fuse-overlayfsKernel >= 4.18FUSE-based
nativeYesCopies layers
vfsYesNo layering

The Limitations

FeatureRootfulRootless
Ports < 1024YesSysctl or capability
macvlanYesNo
ipvlanYesNo
Host networkYesLimited
Resource limitscgroup v1/v2cgroup v2 + systemd
Block storage mountsYesNo
NFS mountsYesNo

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

PitfallWhy It HappensFix
Setup failsuidmap missingInstall the package
Setup failsNo subordinate rangesAdd them to /etc/subuid
Daemon not startingXDG_RUNTIME_DIR unsetSet it in .bashrc
Port 80 failsNo capability or sysctlAdd the sysctl
docker0 missingNetwork namespaceExpected; use published ports
Resource limits ignoredcgroup v1Enable cgroup v2 + systemd
OverlayFS failsKernel without patchUse fuse-overlayfs
docker not foundPATH missing ~/binExport 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

ItemValue
Rootless modeDocker daemon and containers as a non-root user
RootlessKitThe tool that creates the user namespace
newuidmap / newgidmapThe UID/GID mapping tools
/etc/subuid / /etc/subgidThe subordinate ID ranges
dockerd-rootless-setuptool.shThe installer
systemctl --userThe daemon management
slirp4netnsThe default network stack
pastaThe experimental alternative
fuse-overlayfsThe storage driver for most kernels
cgroup v2 + systemdRequired 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 newuidmap and newgidmap tools enforce the mapping, and the /etc/subuid and /etc/subgid files provide the subordinate ranges .
  • The installation requires the uidmap package and subordinate ranges. The dockerd-rootless-setuptool.sh script creates the systemd user unit and the CLI context. It runs as a non-root user and does not require sudo .
  • 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 slirp4netns driver is the default, and pasta is the experimental alternative on Docker 25.0+. The docker0 interface 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_SERVICE capability on the rootlesskit binary or the net.ipv4.ip_unprivileged_port_start=0 sysctl .
  • The storage driver is constrained. OverlayFS works only on Ubuntu and Debian kernels. The fuse-overlayfs snapshotter is the alternative for kernels >= 4.18 .
  • Resource limits require cgroup v2 and systemd. The cgroupfs driver is not supported for rootless mode. The --cpus and --memory flags work only when the host runs cgroup v2 with systemd .
  • The 10.0.2.2 address reaches the host. From inside a slirp4netns container, the host is at 10.0.2.2, not localhost or 127.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!