Running Docker in Rootless Mode: Complete Security Guide
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
dockergroup 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
/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
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:
--privilegeddoes 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.