Normal Docker runs its daemon as root, and anyone who can talk to it effectively has root on the host. Rootless mode runs the daemon and containers as an ordinary user. If an attacker escapes a container, they land as that unprivileged user instead of root.

When it’s worth it

  • Shared servers where several people need to run containers without getting root.
  • CI runners executing untrusted code.
  • Hardening a single-purpose server where you don’t want any root daemon.

For many single-admin servers, standard Docker with careful practices is fine; rootless adds some limitations (see below). Podman is rootless by design — an alternative worth considering: Podman vs Docker.

Prerequisites

Install Docker from Docker’s repository first (guide), then the rootless extras and helper tools:

sudo apt install -y uidmap dbus-user-session docker-ce-rootless-extras slirp4netns

The user needs subordinate UID/GID ranges (usually created automatically for new users):

grep ^$(whoami): /etc/subuid /etc/subgid

If missing:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)

If the system-wide Docker daemon is running and you only want rootless:

sudo systemctl disable --now docker.service docker.socket
sudo rm /var/run/docker.sock

(You can also keep both; they’re separate daemons with separate sockets.)

Install

Log in directly as the target user (via SSH, not su/sudo -u, so a proper user session exists), then:

dockerd-rootless-setuptool.sh install

It sets up a systemd user service and prints environment variables. Add them to ~/.bashrc:

export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

Then:

systemctl --user enable --now docker
sudo loginctl enable-linger $(whoami)       # keep it running without an active login
docker info | grep -i rootless
docker run --rm hello-world

Ports below 1024

Unprivileged processes can’t bind low ports. Options:

  • Publish high ports (-p 8080:80) and run a reverse proxy on the host.
  • Allow unprivileged low ports system-wide:
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-rootless.conf
sudo sysctl --system
  • Or grant the capability to RootlessKit: sudo setcap cap_net_bind_service=ep $(which rootlesskit) and restart the user service.

Limitations

  • Networking: traffic passes through a user-mode network stack (slirp4netns or pasta), which is slower than standard bridge networking for high throughput. Source IPs seen by containers may be the gateway address unless you configure the port driver accordingly.
  • No --privileged equivalents for things that need real root: some kernel features, certain storage drivers and device access won’t work.
  • Resource limits (--memory, --cpus) require cgroup v2 with delegation enabled for the user (default on recent Ubuntu/Debian; check docker info for warnings).
  • Overlay storage works on modern kernels; older ones fall back to slower drivers.
  • Each user has separate images and containers — nothing is shared with the system daemon.

Firewall note

Rootless Docker doesn’t manipulate the host’s iptables the way rootful Docker does, so published ports are subject to your normal firewall rules (UFW works as expected). That’s a pleasant side effect.

Running Compose stacks

Docker Compose works unchanged — it uses DOCKER_HOST. Containers start at boot through the user service plus lingering; set restart: unless-stopped as usual.

Troubleshooting

  • Cannot connect to the Docker daemon at unix:///run/user/1000/docker.sock — the user service isn’t running (systemctl --user status docker) or DOCKER_HOST isn’t set.
  • newuidmap: write to uid_map failed — missing or overlapping subuid ranges.
  • Service stops after logout — enable lingering.
  • systemctl --user fails over sudo -u — log in as the user directly so a user D-Bus session exists.

Related: Docker socket permission errors and Docker security best practices.

  • Docker
  • Security
  • Containers