Rootless Docker: Concepts, Benefits, and Limits
How Docker rootless mode works, how to install it with dockerd-rootless-setuptool, and the practical limits you will hit on ports, cgroups, and storage.
Published on • October 16, 2026
AI Assistant

A container escape is only as catastrophic as the user that runs your daemon. On a default install, dockerd is root, and root in a container is a hop away from root on the host. Rootless mode closes that gap by running the daemon and every container inside a user namespace, so the worst-case outcome of a daemon or runtime compromise is a non-root account. It is not free: privileged ports, networking, cgroups, and storage all pick up constraints. This post covers what actually changes, how to turn it on, and the limits you should plan for before you move a workload over.
What rootless mode changes
Rootless mode executes the Docker daemon and containers inside a user namespace. The docs draw the comparison to userns-remap mode yourself: with userns-remap, the containers are mapped but the daemon still runs with root privileges; in rootless mode, both the daemon and the container run without root privileges. The two modes also differ in how container UIDs and GIDs map to the host.
Three implementation details are worth knowing:
- Rootless mode does not use binaries with
SETUIDbits or file capabilities, with one exception:newuidmapandnewgidmap, which are needed to allow multiple UIDs/GIDs inside the user namespace. - The daemon, its mount namespace, and its network namespace are set up by
rootlesskit; thedockerd-rootless.shwrapper runsdockerdinside those namespaces (the docs even show how tonsenterinto them for debugging). - Container networking runs through a user-mode TCP/IP stack chosen at runtime:
slirp4netnswhen installed, otherwisegvisor-tap-vsock, withvpnkitas a legacy option andpastaavailable as an experimental alternative.
The security benefit follows directly from the first point. There is no privileged daemon to attack, and no setuid helper to abuse; a successful exploit of the daemon or the container runtime lands in an unprivileged user namespace rather than on the host as UID 0. Capabilities you add with --cap-add apply only to resources governed by that container’s user namespace, so they do not grant privileges over host or other global resources.
Prerequisites
Two things must exist before anything else works:
newuidmapandnewgidmap, provided by theuidmappackage on most distributions.- At least 65,536 subordinate UIDs/GIDs in
/etc/subuidand/etc/subgidfor your user.
Check both:
$ whoami
testuser
$ grep ^$(whoami): /etc/subuid
testuser:231072:65536
$ grep ^$(whoami): /etc/subgid
testuser:231072:65536
If the ranges are missing, rootlesskit fails with No subuid ranges found for user ..., and if the ranges are too small you get lchown <FILE>: invalid argument during docker pull. The setup tool prints help when prerequisites are unmet.
Installing
If you installed Docker 20.10 or later with RPM/DEB packages, dockerd-rootless-setuptool.sh is in /usr/bin. Run it as a non-root user:
$ dockerd-rootless-setuptool.sh install
[INFO] Creating /home/testuser/.config/systemd/user/docker.service
...
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger testuser`
[INFO] Creating CLI context "rootless"
Current context is now "rootless"
If the tool is missing, install the extras: sudo apt-get install -y docker-ce-rootless-extras. Without package-manager access, use the script at https://get.docker.com/rootless, which drops binaries in ~/bin (not supported on s390x).
Beforehand, decide what to do about any system-wide daemon:
$ sudo systemctl disable --now docker.service docker.socket
$ sudo rm /var/run/docker.sock
Skipping this means you are still running rootful Docker alongside, and the tool will need --force. Then verify the client is talking to the right daemon:
$ docker info
Client: Docker Engine - Community
Context: rootless
...
Server:
...
Security Options:
seccomp
Profile: builtin
rootless
cgroupns
Expect two environment variables in your shell profile - DOCKER_HOST=unix:///run/user/1000/docker.sock and the PATH entry for the binaries - plus sudo loginctl enable-linger $(whoami) so the user service starts at boot without an interactive login. To remove everything: dockerd-rootless-setuptool.sh uninstall, then rootlesskit rm -rf ~/.local/share/docker for the data.
One environment trap: XDG_RUNTIME_DIR must be set. On systemd hosts pam_systemd provides /run/user/$UID; on non-systemd hosts you create it yourself and delete it at logout. Log in through ssh, the console, or machinectl shell - sudo su breaks systemctl --user.
Networking: what the user-mode stack costs you
All traffic leaves through RootlessKit’s user-mode TCP/IP stack, and the docs are candid that it is slower than the kernel stack. The supported combinations matter when throughput does:
| Network driver | Port driver | Net throughput | Port throughput | Source IP propagation |
|---|---|---|---|---|
gvisor-tap-vsock | builtin | Slow | Fast | Yes (since RootlessKit v3.0) |
slirp4netns | builtin | Slow | Fast | Yes (since RootlessKit v3.0) |
vpnkit | builtin | Slow | Fast | Yes (legacy) |
pasta | implicit | Slow | Fast | Yes (experimental, needs pasta 2023_12_04+) |
lxc-user-nic | builtin | Fast | Fast | Yes (experimental, not SUID-free) |
Pick drivers with a systemd override:
mkdir -p ~/.config/systemd/user/docker.service.d
cat <<'EOF' > ~/.config/systemd/user/docker.service.d/override.conf
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"
EOF
systemctl --user daemon-reload
systemctl --user restart docker
Consequences to budget for: the IPAddress in docker inspect is namespaced inside RootlessKit’s network namespace, so it is unreachable from the host without nsenter (use docker run -p); source IPs are not propagated by default unless you disable userland-proxy in ~/.config/docker/daemon.json; and ping may fail when ping_group_range is 1 0. Since Docker Engine v29.5, docker run --net=host shares the real host network namespace instead of a namespaced one.
Known limits
Storage drivers. Only these are supported: overlay2 (kernel 5.11+), fuse-overlayfs (kernel 4.18+ and the package installed), btrfs (kernel 4.18+, or ~/.local/share/docker mounted with user_subvol_rm_allowed), and vfs. On RHEL 8-era hosts, sudo dnf install -y fuse-overlayfs is the recommended fix. NFS as data-root is not supported; the workaround is a non-NFS data-root in ~/.config/docker/daemon.json.
cgroups. Resource limits work only with cgroup v2 and systemd - the docs call this out explicitly. On cgroup v1, --cpus, --memory, and --pids-limit are silently ignored, and a missing user dbus produces OCI runtime create failed errors that dbus-user-session fixes.
Privileged ports. Binding host port 80 fails with cannot expose privileged port. You have three documented options: publish on 8080 instead, add net.ipv4.ip_unprivileged_port_start=0 to /etc/sysctl.conf, or set CAP_NET_BIND_SERVICE on the rootlesskit binary.
Unsupported features. AppArmor, checkpoint/restore, overlay networks, and exposing SCTP ports are not supported. Distribution quirks sit on top: Ubuntu 24.04+ restricts unprivileged user namespaces by default (the deb-packaged rootless extras bundle the AppArmor profile for rootlesskit; the install script does not), Arch needs kernel.unprivileged_userns_clone=1, and openSUSE/SLES may need modprobe ip_tables iptable_mangle iptable_nat iptable_filter.
Capabilities and devices. --cap-add only reaches resources inside the container’s user namespace; anything requiring capabilities in the initial user namespace still fails. Device and GPU passthrough is not covered by the rootless docs as a supported feature, so treat it as unverified territory: test the exact workload, and expect driver-level permission checks to be the deciding factor rather than Docker configuration.
Kernel knobs you may still touch. If the daemon refuses to start, check cat /proc/sys/kernel/unprivileged_userns_clone (set to 1 if 0) and cat /proc/sys/user/max_user_namespaces (raise it if it is 0 or tiny), then sudo sysctl --system.
A realistic workload check
Before migrating anything, run this list against the service:
dockerd-rootless-setuptool.sh install
systemctl --user status docker
docker info | grep -A4 'Security Options'
docker run --rm -p 8080:80 nginx:alpine # not -p 80:80
docker run --rm --memory=512m --cpus=1 <image> # verify it is not ignored
If the container binds a low port, needs an overlay network, expects AppArmor, or depends on cgroup v1 limits, either reconfigure or keep that workload on a rootful daemon. For development machines, CI runners, and shared hosts where you want contributors running containers without sudo, rootless mode is the right default; for a host that must expose port 443 directly and schedule GPU jobs, the constraints above usually decide the answer.
Key Takeaways
- Rootless mode runs both
dockerdand the containers inside a user namespace, so a daemon or runtime compromise is not root on the host;userns-remapmaps containers but keeps a root daemon. - Prerequisites are
newuidmap/newgidmapplus at least 65,536 entries in/etc/subuidand/etc/subgid;dockerd-rootless-setuptool.sh installsets up a user systemd service and arootlessCLI context. - Traffic goes through a user-mode stack (
slirp4netns,gvisor-tap-vsock,vpnkit, or experimentalpasta) that is slower than the kernel’s;docker inspectIPs are namespaced and-pis the way in. - Hard limits: only
overlay2(5.11+),fuse-overlayfs,btrfs, andvfsstorage; cgroup v2 + systemd for limits; no AppArmor, checkpoint, overlay networks, or SCTP; privileged ports need a sysctl or a higher port. --cap-addstays inside the container’s user namespace, and device/GPU passthrough is not a documented guarantee - verify your workload before moving it.
References: