Docker Rootless Mode: Running Containers Without Root Privileges

Docker runs as root by default, giving containers dangerous host access. Learn how to run Docker in rootless mode, what breaks, and how to fix common compatibility issues.

Y
Yash Pritwani
11 min read read

# Docker Rootless Mode: Running Containers Without Root Privileges

By default, the Docker daemon runs as root. Every container it spawns inherits access to the host kernel with root-level capabilities. This means a container escape — through a kernel vulnerability, a misconfigured mount, or a compromised image — gives the attacker root access to your host.

Docker rootless mode eliminates this by running the entire Docker daemon and all containers as an unprivileged user. No root. No sudo. No elevated capabilities.

Why Docker Runs as Root (and Why That Is a Problem)

Docker needs root for three things:

1. Network namespace creation — setting up virtual networks requires CAP_NET_ADMIN 2. User namespace mapping — mapping container UIDs to host UIDs requires /etc/subuid 3. cgroup management — resource limits (CPU, memory) require cgroup access

Historically, these operations required root. Docker chose convenience over security by running the daemon as root.

The consequences:

A container breakout gives root access to the host
Any process that can talk to the Docker socket (/var/run/docker.sock) effectively has root
Mounting the Docker socket into a container (common for CI/CD) gives that container full host control
# This gives the container full root access to the host
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
# The container can now: create privileged containers, read any file, modify the host

Setting Up Docker Rootless Mode

Prerequisites

# Install uidmap (provides newuidmap/newgidmap)
sudo apt-get install -y uidmap dbus-user-session

# Configure subordinate UID/GID ranges
echo "youruser:100000:65536" | sudo tee -a /etc/subuid
echo "youruser:100000:65536" | sudo tee -a /etc/subgid

# Enable lingering (keeps user services running after logout)
sudo loginctl enable-linger youruser

Installation

# Install rootless Docker (as your regular user, NOT root)
dockerd-rootless-setuptool.sh install

# Set environment variables (add to ~/.bashrc)
export PATH=/home/youruser/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

# Start the rootless daemon
systemctl --user start docker
systemctl --user enable docker

Verify It Works

# Check Docker is running rootless
docker info 2>/dev/null | grep -i "rootless\|security"
# Security Options: rootless

# Check the daemon process
ps aux | grep dockerd
# youruser  12345  ... dockerd-rootless
# Note: NO root process

# Run a container
docker run --rm alpine id
# uid=0(root) gid=0(root)
# The container THINKS it is root, but on the host it maps to youruser

What User Namespaces Actually Do

The key technology behind rootless Docker is user namespaces. Inside the container, processes see themselves as root (UID 0). On the host, they map to an unprivileged UID (e.g., 100000).

Container view:     root (UID 0)
                      |
User namespace map:   UID 0 → UID 100000 on host
                      |
Host view:          youruser:100000 (unprivileged)

This means:

A container breakout lands you as UID 100000 — an unprivileged user with no special access
Files created by root inside the container appear as UID 100000 on host volumes
The container cannot access files owned by actual root (UID 0) on the host

What Breaks in Rootless Mode (and How to Fix It)

Port Binding Below 1024

Rootless containers cannot bind to ports below 1024 (privileged ports):

# This fails in rootless mode
docker run -p 80:80 nginx
# Error: bind: permission denied

# Fix 1: Use a higher port
docker run -p 8080:80 nginx

# Fix 2: Allow unprivileged port binding (Linux kernel 4.11+)
sudo sysctl net.ipv4.ip_unprivileged_port_start=80
# Add to /etc/sysctl.d/rootless.conf for persistence

Volume Permissions

Files written by the container appear as the mapped UID on the host:

docker run -v /home/youruser/data:/data alpine touch /data/test
ls -la /home/youruser/data/test
# -rw-r--r-- 1 100000 100000 0 Mar 23 12:00 test

# Fix: Use --userns-remap or chown inside the container

Overlay2 Storage Driver

Rootless Docker uses fuse-overlayfs instead of the kernel overlay2 driver. Performance is slightly lower for I/O-heavy workloads:

# Check storage driver
docker info | grep "Storage Driver"
# Storage Driver: fuse-overlayfs (rootless)
# vs: overlay2 (rootful)

# For better performance, use overlay2 with idmapped mounts (kernel 5.19+)

Docker Compose

Docker Compose works with rootless Docker, but ensure DOCKER_HOST points to the rootless socket:

export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker compose up -d  # Works normally

Networking Limitations

Rootless containers use slirp4netns or pasta for networking instead of iptables:

No bridge networking — containers communicate via port mapping, not direct IP
Slower than rootful networking — slirp4netns adds overhead
No --net=host — host networking requires root capabilities
# Check which network driver is used
docker info | grep "Network"
# pasta (or slirp4netns)

Rootless Docker in Production

Docker Compose Example

# docker-compose.yml (runs under rootless Docker)
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"    # Use port above 1024
    read_only: true   # Extra hardening
    tmpfs:
      - /tmp
      - /var/cache/nginx

  api:
    image: node:20-slim
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    volumes:
      - ./app:/app:ro
    read_only: true
    tmpfs:
      - /tmp

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

volumes:
  pgdata:

Systemd Service

# ~/.config/systemd/user/docker-compose-app.service
[Unit]
Description=My App (rootless Docker)
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/youruser/app
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=default.target

Rootless vs Other Hardening Approaches

Approach
Protects Against
Overhead
Compatibility

|----------|-----------------|----------|---------------|

Rootless Docker
Container breakout → root access
Low
Some features break
Podman (daemonless)
Same as rootless + no daemon socket
Low
Different CLI
gVisor (runsc)
Kernel exploits
Medium
Syscall compatibility
Kata Containers
Full VM isolation
High
Near-full compatibility
Seccomp profiles
Specific syscall attacks
None
Per-container config

Rootless Docker is the lowest-friction hardening you can apply. It does not replace seccomp or AppArmor — stack them together.

Migration Checklist

Moving from rootful to rootless Docker:

1. Audit port usage — identify anything binding below 1024 2. Check volume permissions — files will be owned by mapped UIDs 3. Test networking — ensure services can communicate via port mapping 4. Update CI/CD — runners need DOCKER_HOST pointing to rootless socket 5. Benchmark I/O — fuse-overlayfs may be slower for database workloads 6. Update monitoring — cAdvisor and Docker metrics need rootless socket path

The Bottom Line

Docker running as root is a design decision from 2013 that prioritized ease of use over security. Rootless mode, available since Docker 20.10, eliminates the most dangerous consequence of container escapes: landing as root on the host.

The compatibility issues are real but manageable. Port binding, volume permissions, and networking all have straightforward fixes. For most workloads, rootless Docker is a drop-in replacement that dramatically reduces your blast radius.

If you are running Docker in production and have not switched to rootless mode, you are one container escape away from full host compromise. The migration takes an afternoon. The security improvement is permanent.

#docker#rootless#container-security#devsecops#linux#security-hardening

Need help with security?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.