What happens when a container breaks out
You wake up one morning to find your server humming along fine, but someone inside has been mining cryptocurrency with your resources. Dig deeper, and you discover the attacker escalated privileges from inside a Docker container — then took over the entire machine simply because the Docker daemon was running as root.
That’s not a hypothetical. I’ve seen it happen in practice on servers running Docker with default settings — the alarming part is that most sysadmins have no idea this risk is sitting right there.
Why is the default Docker setup so dangerous?
When you install Docker following the official guide, dockerd (the Docker daemon) runs as root and listens on the /var/run/docker.sock socket. Any process that can connect to this socket can do anything: mount system directories into containers, run privileged containers, even read and write arbitrary files on the host system.
The typical attack chain looks like this:
- Attacker exploits a vulnerability in an application running inside a container
- From within the container, they access the Docker socket if it’s been bind-mounted in
- They use the Docker API to create a new container, mounting the host’s
/into it - They chroot into the host filesystem — the entire machine is now under their control
Many people think: “I don’t bind-mount the socket into my containers, so I’m safe.” Less risky, sure. But other vectors remain: container escapes, runc vulnerabilities (CVE-2019-5736 once allowed escaping a container by executing a specially crafted binary), namespace breakouts. When a container breaks out, it lands in the root namespace on the host — game over.
Three common approaches — and why they fall short
Approach 1: Add the user to the docker group
sudo usermod -aG docker youruser
This sounds reasonable, but members of the docker group can do everything the Docker daemon can — which is effectively equivalent to root on the host. The official Docker documentation explicitly states this is a “docker group grants privileges equivalent to the root user” situation. Adding developers to the docker group for convenience is functionally giving them root access.
Approach 2: Use sudo with a command whitelist
# /etc/sudoers.d/docker-restricted
developer ALL=(root) NOPASSWD: /usr/bin/docker run *
Cumbersome to maintain, easy to bypass via argument injection, and it still doesn’t address the root cause — the daemon still runs as root.
Approach 3: Use AppArmor/SELinux to restrict containers
Useful, but it’s an additional layer — it doesn’t change the fact that the daemon runs as root. If the daemon is compromised, the profile protects the containers, not the daemon itself.
Rootless Docker: Cutting privilege at the root
Since Docker 20.10, Rootless mode has been a stable feature — allowing both the daemon and containers to run inside an ordinary user namespace, with no root privileges at any point. Even if an attacker escapes a container, they only have that user’s privileges on the host — no path to escalate to system root.
Prerequisites
- Linux kernel 5.11+ (or 4.18+ with additional configuration)
- The
uidmappackage installed (newuidmap,newgidmap) - Your user has entries in
/etc/subuidand/etc/subgid - No sudo required to run — that’s the whole point
Installation on Ubuntu/Debian
# Install uidmap
sudo apt-get install -y uidmap
# Check if subuid/subgid entries already exist
grep $(whoami) /etc/subuid /etc/subgid
# If no entries exist, add them:
echo "$(whoami):100000:65536" | sudo tee -a /etc/subuid
echo "$(whoami):100000:65536" | sudo tee -a /etc/subgid
# Run the rootless Docker setup script
dockerd-rootless-setuptool.sh install
# Or use the convenience script:
curl -fsSL https://get.docker.com/rootless | sh
Setting environment variables
# Add to ~/.bashrc or ~/.zshrc
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
source ~/.bashrc
Starting the daemon with systemd user
# Enable and start the service at user level — no sudo needed
systemctl --user enable docker
systemctl --user start docker
# Enable lingering so the daemon runs even when the user is logged out
sudo loginctl enable-linger $(whoami)
# Check status
systemctl --user status docker
Verifying it’s actually rootless
# Run a container and check the UID inside
docker run --rm alpine id
# Output: uid=0(root) gid=0(root) — looks like root but this is a UID namespace
# Check the actual UID of the process on the host
docker run -d --name test nginx
ps aux | grep nginx | head -3
# The nginx PID will run under your UID on the host, NOT 0
Here’s the interesting part: inside the container, the process sees itself as root (uid=0), but on the host it runs under your UID — the user namespace acts as a translation layer between the two. The container thinks it has root privileges, but it can’t touch anything on the host system.
Day-to-day workflow with Rootless Docker
# Run a web server as usual
docker run -d -p 8080:80 --name webserver nginx
# Docker Compose works exactly the same
docker compose up -d
# Build an image
docker build -t myapp:latest .
# View logs
docker logs webserver
Most day-to-day workflows are unchanged — build, run, compose, and logs all work exactly as before.
Limitations to know upfront
Rootless Docker does come with trade-offs — some things aren’t possible or require workarounds:
- Binding ports below 1024: Regular users can’t bind ports <1024. You’ll need to use sysctl or map to a high port and use a reverse proxy.
- Overlay filesystem: Requires kernel 5.11+ or FUSE-overlayfs. On older kernels it falls back to the VFS driver — 2–3x slower than overlay2 on write-heavy workloads.
- Cgroups v1: Resource limits (memory/CPU) require cgroups v2. Ubuntu 22.04+ uses v2 by default, so this is a non-issue.
- Host network mode:
--net=hostdoesn’t use the actual host network namespace — the container only gets your user network namespace.
# Workaround for ports 80/443: allow users to bind low-numbered ports
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless.conf
sudo sysctl -p /etc/sysctl.d/99-rootless.conf
# Or keep high ports and use Caddy/nginx as a reverse proxy
docker run -d -p 8080:80 nginx
# Then have Caddy/nginx on the host proxy :80 → localhost:8080
Combining with a broader security posture
Rootless Docker closes a major attack surface, but server security doesn’t stop there. Alongside it, I regularly audit credentials — making sure passwords are strong and not reused. I use the password generator at toolcraft.app/en/tools/security/password-generator for generating server passwords — it runs entirely in the browser, so nothing leaks over the network. Combined with SSH key-based authentication, that covers the authentication layer solidly.
For new servers, I treat Rootless Docker as the default config rather than an optional extra — setup takes about 10 extra minutes but closes a risk layer that the default installation leaves wide open. It’s even more valuable on shared dev servers: each person gets their own daemon, one person’s containers don’t affect another’s. And nobody has a shortcut to host root — whether by accident or intent.
