Skip to content
Go back

Rootless Docker: Tips, Gotchas & Fixes

By SumGuy 12 min read
Rootless Docker: Tips, Gotchas & Fixes

You Installed Rootless Docker. Now What?

Getting rootless Docker running is the easy part — the install walkthrough covers that. This article is everything that comes after: the env vars nobody tells you about, the networking decisions you’ll regret not making upfront, the gotchas that surface at 2 AM, and the real error messages (with real fixes).

One more thing before we dive in: this article is anchored to Docker Engine 29.6.0, released June 18, 2026, and reflects dockerd-rootless.sh and its env vars as they exist in moby/moby master / Docker Engine 29.x with RootlessKit v3.x. Env var names and their defaults do shift between releases. If you’re on something significantly older or newer, cross-check with dockerd-rootless.sh --help and the comments at the top of /usr/bin/dockerd-rootless.sh. Version-stamping is the whole point here — a lot of rootless Docker troubleshooting advice floating around was written against an older stack and will silently mislead you.


The Mental Model (Thirty Seconds, Then We Move On)

In rootless mode, both the Docker daemon and your containers run inside a user namespace. This is different from userns-remap, where the daemon itself still runs as root. With rootless, no SETUID binaries are in the picture — except newuidmap and newgidmap from the uidmap package, which handle the UID/GID mapping at the boundary.

The part that bites people is the networking. Because the daemon runs unprivileged, actual network connectivity between containers and the outside world goes through RootlessKit, a userspace networking layer. That’s where all the interesting decisions — and most of the gotchas — live.

A working setup looks like this in docker info:

Terminal window
docker info | grep -A5 "Security Options"
# Security Options: rootless cgroupns seccomp Profile: builtin
docker context show
# rootless

If you see that, you’re in business. Let’s talk about tuning it.


The Env Vars That Actually Matter

RootlessKit’s behavior is controlled by the DOCKERD_ROOTLESS_ROOTLESSKIT_* family of environment variables, set on the dockerd-rootless.sh process. The canonical way to apply them when running under systemd is a drop-in override:

~/.config/systemd/user/docker.service.d/override.conf
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"

After editing: systemctl --user daemon-reload && systemctl --user restart docker. That’s the method. Now, what can you put in there?

DOCKERD_ROOTLESS_ROOTLESSKIT_NET — the network driver. Options: slirp4netns, vpnkit, pasta, gvisor-tap-vsock, lxc-user-nic. (The value host is explicitly rejected — it won’t work.) Default selection order: slirp4netns if version >= 0.4.0 is installed, else pasta (as of 28.5.2), else vpnkit, else gvisor-tap-vsock.

DOCKERD_ROOTLESS_ROOTLESSKIT_MTU — MTU for the virtual network. Default: 65520 for slirp4netns, pasta, and gvisor-tap-vsock; 1500 for other drivers. You rarely need to touch this unless you’re fighting packet fragmentation issues upstream.

DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER — how ports are forwarded. Options: builtin, slirp4netns, implicit, gvisor-tap-vsock. Default: implicit when using the pasta network driver, builtin for everything else.

DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX — wrap slirp4netns in a dedicated mount namespace. Values: auto (default), true, false. Leave it on auto.

DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP — add seccomp to slirp4netns. Same auto/true/false options and default. Same advice: leave it.

DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK — when true (the default), containers can’t reach 127.0.0.1 on the host, including via the slirp4netns gateway at 10.0.2.2. This is a real security boundary. Only flip it to false if you have a specific need and know what you’re exposing.

DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS — “detach-netns” mode, default true. Makes docker pull/push/build faster and enables --net=host. Set it to false only if you hit a specific compatibility problem. Honestly, you won’t.

DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR — RootlessKit state dir, default $XDG_RUNTIME_DIR/dockerd-rootless. You almost never need to touch this.

DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS — raw flags appended to the rootlesskit invocation. Main real-world use: publishing the Docker API over TCP (more on that below).


Picking a Network Driver (The Table You Actually Want)

Here’s the driver-selection guide straight out of the script. The throughput, source-IP, and No-SUID columns are taken verbatim from the script’s own table; the notes are my plain-English gloss:

Network driverPort driverNet throughputPort throughputSource IPNo SUIDNotes
slirp4netnsbuiltinSlowFastyes(*)yesAuto-default when slirp4netns installed
gvisor-tap-vsockbuiltinSlowFastyes(*)yesAuto-fallback when slirp4netns not installed
pastaimplicitSlowFastyesyesExperimental; needs pasta >= 2023_12_04
slirp4netnsslirp4netnsSlowSlowyesyesSource IP without v3.0 requirement
gvisor-tap-vsockgvisor-tap-vsockSlowSlownoyesNot recommended — use builtin port driver
vpnkitbuiltinSlowFastyes(*)yesLegacy
lxc-user-nicbuiltinFastFastyes(*)NOExperimental; requires a SUID binary

(*) Source IP preservation with the builtin port driver requires RootlessKit v3.0+ AND disabling the userland-proxy. If you’re on Docker 29.x with RootlessKit v3.x, you have v3.0+.

The honest summary:

Everything except lxc-user-nic shows “Slow” for raw network throughput — that’s the userspace networking tax. For typical homelab workloads (web apps, databases, media servers) you won’t notice. Network-benchmark-heavy workloads will.

For most people: slirp4netns + builtin port driver. Auto-default when slirp4netns is installed, fast port throughput, just works.

If slirp4netns isn’t installed: gvisor-tap-vsock + builtin auto-kicks in. Also fine.

pasta is the one to watch — second-choice fallback since 28.5.2, native source IP, clearly where things are heading. Still experimental, needs a recent build.

lxc-user-nic is the only “Fast” option but requires a SUID binary. That’s the thing rootless mode avoids. Skip it unless you have a compelling reason.


The Source IP Problem (And Whether You Actually Have It)

By default, docker run -p does not propagate the real client source IP to your container. The container sees the RootlessKit gateway IP instead. This is a documented limitation.

The path to fixing it:

If you need real source IPs and you’re reverse-proxying anyway (which you should be — see the networking article), let the reverse proxy handle it via X-Forwarded-For / X-Real-IP. That’s cleaner than fighting the port driver layer.


The Gotchas Section (Read Before You Hit Them)

Container IPs Are Not Reachable from the Host

docker inspect shows an IPAddress like 172.17.0.2. You curl it from the host. Nothing. That IP lives inside RootlessKit’s network namespace — it’s not reachable from the host without nsenter-ing into that namespace.

Use -p port publishing to expose services, or point your reverse proxy at localhost:<published-port>. The container IP is for container-to-container traffic, not host-to-container access.

No Overlay Networking (No Multi-Host Swarm)

Rootless Docker does not support the overlay network driver. If you’re trying to set up Docker Swarm across multiple hosts with overlay networks, rootless mode won’t get you there. For multi-host container orchestration from a rootless setup, look at k3s or similar.

Supported Storage Drivers (Know Before You Pull)

Only four storage drivers work in rootless mode:

If you’re on RHEL 8, you’ll likely need fuse-overlayfs (sudo dnf install -y fuse-overlayfs). RHEL 9+ handles it differently. docker pull failing with layer-registration errors is often a storage driver mismatch.

NFS as Your Data Root Is Not Supported

~/.local/share/docker must not be on NFS — this applies to rootful Docker too, but it catches people whose home directories are NFS-mounted in enterprise environments. Override data-root in ~/.config/docker/daemon.json to point somewhere local if that’s you.

ping Doesn’t Work Inside Containers

Unprivileged users can’t send ICMP by default, so ping in your containers fails silently. One-time host fix:

Terminal window
echo "net.ipv4.ping_group_range = 0 2147483647" | sudo tee /etc/sysctl.d/99-ping.conf
sudo sysctl --system

Privileged Ports (< 1024)

Trying to bind port 80 or 443 directly will give you something like:

cannot expose privileged port 80, you might need to add "net.ipv4.ip_unprivileged_port_start=0" (currently 1024) to /etc/sysctl.conf, or set CAP_NET_BIND_SERVICE on rootlesskit binary, or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied.

Three ways out:

  1. Just don’t bind 80/443 directly. Publish to 8080/8443 and put a reverse proxy in front. This is the right answer for most setups anyway.

  2. Grant that one capability to rootlesskit:

    Terminal window
    sudo setcap cap_net_bind_service=ep $(which rootlesskit)
    systemctl --user restart docker

    Surgical. Only rootlesskit gets the capability.

  3. Lower the unprivileged port floor system-wide:

    Terminal window
    sudo sysctl net.ipv4.ip_unprivileged_port_start=0

    This works, but it affects every user on the system. Fine for a single-user homelab box; be aware of the blast radius on shared systems.

containerd-rootless Conflict

If you’ve also been running containerd-rootless.sh (e.g. for nerdctl or a standalone containerd setup), dockerd-rootless.sh will refuse to start if it finds the containerd-rootless state dir at $XDG_RUNTIME_DIR/containerd-rootless. You’ll get an explicit error telling you to stop containerd-rootless and remove that directory. It’s not subtle — just do what it says.


cgroup Resource Limits: The Silent Non-Apply Problem

CPU, memory, and pids limits (--cpus, --memory, --pids-limit) only work under rootless Docker with cgroup v2 AND systemd as the cgroup driver. Check docker info | grep "Cgroup Driver" — if you see systemd, the plumbing is there.

Here’s the gotcha: by default, systemd only delegates the memory and pids controllers to non-root user slices. That means --memory and --pids-limit work, but --cpus, --cpuset, and I/O limits silently don’t. You set them, Docker accepts them, nothing actually enforces them. Fun.

Fix it as root:

/etc/systemd/system/[email protected]/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
Terminal window
sudo systemctl daemon-reload
# Re-login or restart your user session to apply

This enables delegation of all five controllers for every user session on the machine. After re-login, --cpus and cpuset limits will actually do something. (One caveat: delegating cpuset specifically needs systemd 244 or later — on anything modern you’re fine.)


Troubleshooting: Real Errors, Real Fixes

operation not permitted on startup

[rootlesskit:parent] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted

This means unprivileged user namespaces are restricted or disabled on your system.

Ubuntu 24.04+: ships with restricted user namespaces enabled. If you installed via the docker-ce-rootless-extras deb package, an AppArmor profile for rootlesskit is bundled — nothing to do. If you used the get.docker.com/rootless script, you need to manually add an AppArmor profile for your rootlesskit binary under /etc/apparmor.d/, then restart the AppArmor service.

Arch Linux: check cat /proc/sys/kernel/unprivileged_userns_clone — if that’s 0, it’s disabled. Fix:

Terminal window
echo "kernel.unprivileged_userns_clone=1" | sudo tee /etc/sysctl.d/99-userns.conf
sudo sysctl --system

Cannot connect to the Docker daemon After Login or Reboot

Checklist in order:

  1. Is the service running? systemctl --user status docker
  2. Is DOCKER_HOST set? Should show unix:///run/user/$UID/docker.sock
  3. Is linger enabled? Without it, user services die on logout:
    Terminal window
    sudo loginctl enable-linger $(whoami)
  4. After any config change: systemctl --user daemon-reload && systemctl --user restart docker

Linger is the most commonly-missed step. Daemon runs fine in your session, you log out, it’s gone — that’s linger not being set.

docker pull Failing with Layer/Storage Errors

Almost always a storage driver issue. Check docker info | grep "Storage Driver". On RHEL 8-style distros, install fuse-overlayfs: sudo dnf install -y fuse-overlayfs && systemctl --user restart docker. Also confirm ~/.local/share/docker is on a local filesystem, not NFS.

No Network Connectivity (Distro-Specific)

On openSUSE/SLES, iptables kernel modules may not be loaded:

Terminal window
sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter

On RHEL/Fedora, you may need:

Terminal window
sudo dnf install -y iptables

If you’ve tried everything else and network is still broken, fall back to the most-compatible driver combo via the systemd override:

~/.config/systemd/user/docker.service.d/override.conf
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"

This is slower on port throughput but source IP works and it’s the most battle-tested combination. Good for debugging — once things work, you can switch back to the faster combo.


Quick Bonus: Docker-in-Docker and the TCP API

Rootless DinD: use docker:<version>-dind-rootless (not dind). It runs as UID 1000. --privileged is still required to disable seccomp, AppArmor, and mount masks inside the nested environment: docker run -d --name dind-rootless --privileged docker:29-dind-rootless.

TCP API: use DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS to publish the port through RootlessKit, always with TLS. See the TCP socket article for the cert setup:

Terminal window
DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="-p 0.0.0.0:2376:2376/tcp" \
dockerd-rootless.sh \
-H tcp://0.0.0.0:2376 \
--tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem

Set the FLAGS value in your systemd override rather than on the command line when managing the daemon with systemd.


The Payoff

Rootless Docker is genuinely good. The security model is tighter, the surface area is smaller, and for most homelab workloads the performance tradeoffs are invisible. But it’s also a different enough runtime that the standard Docker mental model — “I can reach container IPs from the host,” “overlay networks just work,” “limits apply automatically” — gets you into trouble fast.

Know where the network namespace boundary is. Set up cgroup delegation once and forget about it. Enable linger. And if something breaks, check the storage driver and the AppArmor policy before you spiral into two hours of kernel parameter archaeology.

Your 2 AM self will appreciate having read this first.



Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
cert-manager: ACME at Scale
Next Post
Argo Rollouts vs Flagger Progressive Delivery

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts