The Docker daemon traditionally runs as root, which means any container escape vulnerability gives an attacker root access to the host. This is not a theoretical concern—CVE-2019-5736 (runc container escape) demonstrated exactly this attack. Rootless mode eliminates this entire class of risk by running the Docker daemon and all containers inside a user namespace, where root inside the container maps to an unprivileged user on the host.

This guide covers the complete process of setting up rootless Docker, understanding its limitations, and working around them for production deployments.

Why Rootless Mode Matters

In standard Docker, the attack surface is significant:

  • The Docker daemon runs as root (PID 1 level privileges)
  • Containers share the host kernel
  • A container escape grants full root access to the host
  • Anyone in the docker group effectively has root equivalent access
  • Volume mounts can read/write any host file

Rootless mode changes the security model fundamentally:

Aspect Standard Docker Rootless Docker
Daemon runs as root Unprivileged user
Container escape impact Full root access Unprivileged user access
Volume mount access Any file on host Only user-owned files
Port binding Any port (including <1024) Ports >=1024 only (by default)
cgroup management Full cgroup access cgroup v2 with delegation
Network performance Native Slight overhead (slirp4netns/pasta)

Prerequisites

Rootless mode requires specific kernel features and system configuration:

# Check kernel version (5.11+ recommended, 4.18+ minimum)
uname -r

# Verify user namespaces are enabled
cat /proc/sys/kernel/unprivileged_userns_clone
# Should output: 1

# If not enabled (Debian/Ubuntu):
sudo sysctl -w kernel.unprivileged_userns_clone=1
echo "kernel.unprivileged_userns_clone=1" | sudo tee /etc/sysctl.d/99-rootless.conf

# Check for subordinate UID/GID ranges
grep $USER /etc/subuid
grep $USER /etc/subgid
# Should show something like: username:100000:65536

# If missing, add them:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

# Install required packages
# Debian/Ubuntu:
sudo apt-get install -y uidmap dbus-user-session fuse-overlayfs slirp4netns

# Fedora/RHEL:
sudo dnf install -y shadow-utils fuse-overlayfs slirp4netns
Warning: If your system does not have /etc/subuid and /etc/subgid entries for your user, rootless Docker will fail to start. The subordinate UID/GID ranges define the user namespace mapping.

Installation

Method 1: Using the Official Script

# If Docker Engine is already installed, install just the rootless extras
dockerd-rootless-setuptool.sh install

# If Docker is NOT installed, install everything from scratch
curl -fsSL https://get.docker.com/rootless | sh

# The script outputs environment variables to set:
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

# Add these to your shell profile
echo 'export PATH=/home/$USER/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc

Method 2: Manual Installation

# Stop the system-wide Docker daemon (if running)
sudo systemctl disable --now docker.service docker.socket

# Install rootless Docker
dockerd-rootless-setuptool.sh install

# Enable the rootless daemon to start on login
systemctl --user enable docker
systemctl --user start docker

# Make the daemon persist across logouts (linger)
sudo loginctl enable-linger $USER

Verifying the Installation

# Check that rootless Docker is running
docker info 2>&1 | grep -i "rootless\|security"

# You should see:
#  Security Options: rootless
#  rootless

# Run a test container
docker run --rm hello-world

# Verify the daemon is NOT running as root
ps aux | grep dockerd
# Should show your username, NOT root

Configuration

Daemon Configuration

The rootless daemon uses a per-user configuration file:

# ~/.config/docker/daemon.json
{
  "storage-driver": "fuse-overlayfs",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "default-address-pools": [
    {"base": "172.17.0.0/16", "size": 24}
  ]
}

Storage Driver Selection

Rootless mode supports different storage drivers depending on your kernel and filesystem:

Driver Requirement Performance Recommended
overlay2 (native) Kernel 5.11+ with unprivileged overlay Best Yes, if kernel supports it
fuse-overlayfs fuse-overlayfs package Good Yes, fallback for older kernels
vfs None Poor (no CoW) Last resort only
# Check if native overlay is available for rootless
# Kernel 5.11+ on Ubuntu 22.04+, Fedora 34+
unshare -rm sh -c 'mkdir -p l u w m && mount -t overlay overlay -o lowerdir=l,upperdir=u,workdir=w m'
# If this succeeds, native overlay2 works in rootless mode

Networking in Rootless Mode

Networking is the area with the most significant differences from standard Docker. Since rootless Docker cannot create real network namespaces or manipulate iptables, it uses userspace networking:

slirp4netns (Default)

The default networking driver for rootless Docker. It provides NAT networking entirely in userspace:

# slirp4netns is used automatically
docker run -p 8080:80 nginx:alpine
# Container port 80 is mapped to host port 8080

pasta (Recommended on Modern Systems)

pasta (part of the passt project) provides better performance than slirp4netns:

# Install pasta
sudo apt-get install passt  # Debian/Ubuntu
sudo dnf install passt      # Fedora

# Configure Docker to use pasta
# ~/.config/docker/daemon.json
{
  "network-control-plane": "pasta"
}

Port Binding Restrictions

# By default, rootless Docker cannot bind to ports below 1024
docker run -p 80:80 nginx:alpine
# Error: failed to bind port 80

# Solution 1: Use a port above 1024
docker run -p 8080:80 nginx:alpine

# Solution 2: Lower the unprivileged port start (system-wide)
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless-ports.conf

# Solution 3: Use a reverse proxy (nginx, Caddy) running on the host
Tip: For production deployments, the recommended approach is to run a reverse proxy (like Caddy or Nginx) on the host that forwards to rootless Docker containers on high ports. This provides TLS termination as a bonus.

Storage Considerations

Rootless Docker stores all data under the user's home directory by default:

# Default data root
ls ~/.local/share/docker/

# To change the data root:
# ~/.config/docker/daemon.json
{
  "data-root": "/data/docker-rootless"
}

# Ensure the directory is owned by your user
mkdir -p /data/docker-rootless
# No sudo needed - you must own this directory

Volume Permissions

Volume permissions can be confusing in rootless mode because of user namespace remapping:

# Inside the container, root is UID 0
# On the host, this maps to your subordinate UID range (e.g., 100000)

# This means:
docker run -v /tmp/data:/data alpine touch /data/testfile
ls -la /tmp/data/testfile
# Owner will be 100000:100000 (your subordinate UID), NOT root

# To work around this in development:
docker run --user $(id -u):$(id -g) -v /tmp/data:/data alpine touch /data/testfile
# File will be owned by your actual UID

User Namespace Remapping (userns-remap)

For standard (root) Docker, you can enable user namespace remapping as an alternative to full rootless mode. This remaps the root user inside containers to an unprivileged user on the host:

# /etc/docker/daemon.json (for standard Docker, not rootless)
{
  "userns-remap": "default"
}

# This creates a dockremap user and uses its subordinate UIDs
# Docker creates entries in /etc/subuid and /etc/subgid automatically

# Restart Docker
sudo systemctl restart docker

# Verify
docker run --rm alpine cat /proc/self/uid_map
# 0     231072      65536
# Root (0) inside container maps to UID 231072 on host

The key difference: userns-remap still runs the Docker daemon as root but remaps UIDs inside containers. Rootless mode runs the entire daemon as an unprivileged user.

Known Limitations

  • No privileged containers: --privileged does not work in rootless mode
  • Limited AppArmor/SELinux: Some security profiles may not apply correctly
  • No low ports by default: Cannot bind to ports below 1024 without sysctl modification
  • Networking overhead: ~5-10% overhead compared to standard Docker networking
  • Some volume mount restrictions: Cannot mount arbitrary host paths owned by other users
  • cgroup v2 required: For resource limits (CPU, memory) to work
  • No ping: ICMP requires root privileges; containers cannot use ping
  • Docker Swarm: Not supported in rootless mode

Checking cgroup v2

# Verify cgroup v2 is enabled
stat -fc %T /sys/fs/cgroup/
# Should output: cgroup2fs

# If still using cgroup v1, add to kernel boot parameters:
# systemd.unified_cgroup_hierarchy=1
# Then reboot

# With cgroup v2, resource limits work:
docker run --memory 256m --cpus 0.5 alpine cat /proc/self/cgroup

Troubleshooting

Daemon Fails to Start

# Check the rootless Docker service logs
journalctl --user -u docker

# Common issues:
# 1. Missing subuid/subgid entries
grep $USER /etc/subuid /etc/subgid

# 2. XDG_RUNTIME_DIR not set
echo $XDG_RUNTIME_DIR
# Should be /run/user/$(id -u)

# 3. Linger not enabled (daemon stops on logout)
loginctl show-user $USER | grep Linger
sudo loginctl enable-linger $USER

# 4. Conflicting system Docker
sudo systemctl status docker
# If running, it may conflict. Disable it or use a different DOCKER_HOST.

Permission Denied Errors

# "permission denied" when accessing volumes
# Check the UID mapping
docker run --rm alpine cat /proc/self/uid_map

# Ensure your user owns the mount point
ls -la /path/to/mount

# Use --user flag to match host UID
docker run --user $(id -u):$(id -g) -v /path:/data myimage

Network Issues

# "bind: permission denied" for low ports
# Either use high ports or modify sysctl:
sudo sysctl net.ipv4.ip_unprivileged_port_start=0

# DNS resolution failures inside containers
# Check /etc/resolv.conf inside the container
docker run --rm alpine cat /etc/resolv.conf

# If DNS fails, specify DNS servers explicitly
docker run --dns 8.8.8.8 --rm alpine nslookup google.com

Performance Impact

The performance overhead of rootless mode is minimal for most workloads:

Workload Overhead vs Standard Docker Notes
CPU-bound <1% Negligible
Memory-bound <1% Negligible
Disk I/O (overlay2 native) <1% Requires kernel 5.11+
Disk I/O (fuse-overlayfs) 5-15% FUSE overhead
Network I/O (slirp4netns) 10-20% Userspace networking
Network I/O (pasta) 3-8% Better than slirp4netns

For most web applications, APIs, and batch processing workloads, rootless mode performs nearly identically to standard Docker. The overhead is most noticeable in network-intensive and disk I/O-intensive workloads on older kernels.

Integrating with Management Tools

When using rootless Docker with management platforms like usulnet, point the tool to your user-level Docker socket:

# The rootless Docker socket location
echo $DOCKER_HOST
# unix:///run/user/1000/docker.sock

# Or equivalently
ls /run/user/$(id -u)/docker.sock

Management tools that connect via the Docker API work identically with rootless Docker—they just need to be pointed at the correct socket path.

Conclusion

Rootless Docker is production-ready and should be the default for any security-conscious deployment. The limitations around privileged containers and low port binding are easily worked around with reverse proxies and proper architecture. The security benefit—eliminating root-level container escapes as a threat vector—far outweighs the minor inconveniences.

Start by testing rootless mode in development. Once you are comfortable with the workflow differences, migrate your production hosts. With modern kernels (5.11+) and cgroup v2, the experience is nearly identical to standard Docker.