Rootless Docker: Setup, Limitations & Fixing Permission Errors
Rootless Docker lets you run the Docker daemon and containers as a non-root user, reducing the blast radius of daemon or container escapes and making Docker feasible in locked-down environments (shared servers, corporate laptops, CI runners without privileged access). This tutorial walks through installation, how it works, common limitations, and—most importantly—how to fix the permission errors that show up when switching from “rootful” Docker.
Table of Contents
- What “Rootless Docker” Actually Means
- Prerequisites and Concepts
- Install Rootless Docker (Linux)
- Using Rootless Docker Day-to-Day
- Networking in Rootless Mode
- Storage & Filesystem Behavior
- Fixing Common Permission Errors (with Real Causes)
permission deniedon the Docker socketcannot create /var/run/docker.sock: no such file or directorynewuidmap/newgidmaperrorswrite /proc/...: permission deniedand missing capabilities- Bind mounts:
permission denied/ wrong ownership inside container - Volumes and SELinux/AppArmor gotchas
- “Operation not permitted” when using
ping,iptables, or raw sockets - Systemd user service issues
- Security Notes and Trade-offs
- Troubleshooting Checklist
- Uninstall / Reset Rootless Docker
What “Rootless Docker” Actually Means
In traditional (rootful) Docker:
dockerdruns as root- containers are created by a privileged daemon
- the Docker socket is typically
/var/run/docker.sock(root-owned) - the daemon can manipulate host networking, cgroups, mounts, iptables, etc.
In rootless Docker:
dockerdruns as your user- containers run in a user namespace, where “root inside the container” is mapped to an unprivileged UID on the host
- the Docker socket lives under your home directory (or user runtime dir), not
/var/run - networking is implemented in user space (commonly
slirp4netns), not via host iptables rules - some kernel features are restricted because you’re not root
The core benefit: even if a container breaks out, it typically lands in your user’s permissions, not host root.
Prerequisites and Concepts
User namespaces and subordinate IDs
Rootless containers rely on user namespaces. The kernel maps container UIDs/GIDs to a range of “subordinate” IDs on the host.
You need entries like:
/etc/subuid:username:100000:65536/etc/subgid:username:100000:65536
This says: “map container IDs into host IDs starting at 100000, for 65536 IDs”.
Also required: newuidmap and newgidmap (from the uidmap package on many distros). These helpers set up the mappings safely.
Rootless networking (slirp4netns / vpnkit)
Rootless Docker typically cannot create privileged network namespaces and iptables rules the same way rootful Docker does. Instead it uses user-space networking:
- slirp4netns: common on Linux
- vpnkit: sometimes used, especially in Docker Desktop contexts
This affects performance and features (e.g., no true L2 integration, different port-forwarding behavior).
Storage drivers (fuse-overlayfs vs vfs)
Rootless Docker often uses:
fuse-overlayfs(preferred when kernel overlay isn’t usable)overlay2(possible on some kernels/configurations)vfs(fallback; slow and disk-heavy)
Your storage driver impacts performance and compatibility.
Install Rootless Docker (Linux)
The official Docker packages include a rootless setup tool. The exact package names differ by distro, but the flow is consistent.
1) Remove conflicts / understand coexistence
You can keep rootful Docker installed and also run rootless Docker for your user. They are separate daemons with separate sockets.
Check if rootful Docker is installed/running:
systemctl status docker
docker info 2>/dev/null | head
If you want to stop rootful Docker (optional):
sudo systemctl disable --now docker.service docker.socket
2) Install dependencies
On Debian/Ubuntu:
sudo apt-get update
sudo apt-get install -y uidmap dbus-user-session slirp4netns fuse-overlayfs
On Fedora:
sudo dnf install -y uidmap slirp4netns fuse-overlayfs
On Arch:
sudo pacman -S --needed uidmap slirp4netns fuse-overlayfs
Install Docker Engine (example for Debian/Ubuntu using Docker’s repo is omitted here for brevity), then confirm you have the rootless tool:
dockerd-rootless-setuptool.sh --help
If that script isn’t found, your Docker package may be incomplete or installed via a method that doesn’t ship it.
3) Configure /etc/subuid and /etc/subgid
Check current mappings:
grep -E "^$USER:" /etc/subuid /etc/subgid
If nothing is returned, create mappings (choose a free range; 100000 is common):
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"
Verify:
grep -E "^$USER:" /etc/subuid
grep -E "^$USER:" /etc/subgid
Log out and back in (or reboot) so your session picks up changes cleanly.
4) Run the rootless setup tool
Run as your normal user (no sudo):
dockerd-rootless-setuptool.sh install
Typical output includes:
- where the socket will be created
- how to enable the user systemd service
- environment variables to set
If your system uses systemd, the tool usually enables a user service:
systemctl --user status docker
Start it if needed:
systemctl --user start docker
systemctl --user enable docker
Enable “linger” if you want Docker to run even when you’re not logged in:
sudo loginctl enable-linger "$USER"
5) Configure environment variables
Rootless Docker uses a different socket path. Commonly:
$XDG_RUNTIME_DIR/docker.sock(often/run/user/<uid>/docker.sock)- or
~/.docker/run/docker.sock
Set DOCKER_HOST accordingly. The setup tool often suggests:
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
To make it persistent, add to ~/.bashrc or ~/.profile:
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
Reload:
source ~/.bashrc
6) Validate the installation
Check that your client is talking to the rootless daemon:
docker context ls
docker info
In docker info, look for hints like:
- rootless mode enabled
- cgroup driver / version
- storage driver (often
fuse-overlayfs)
Run a test container:
docker run --rm hello-world
docker run --rm -it alpine sh -lc 'id && echo OK'
Inside the container, id may show uid=0(root) but that “root” is mapped to your unprivileged host IDs.
Using Rootless Docker Day-to-Day
Switching between rootless and rootful contexts
If you have both daemons, use Docker contexts to avoid constantly exporting DOCKER_HOST.
Create a context for rootless (adjust socket path):
docker context create rootless --docker "host=unix:///run/user/$(id -u)/docker.sock"
Rootful usually uses:
docker context create rootful --docker "host=unix:///var/run/docker.sock"
Switch:
docker context use rootless
docker info | grep -i rootless -n || true
docker context use rootful
docker info | head
Docker Compose with rootless
Compose uses the same Docker socket as the docker CLI. If DOCKER_HOST or context is correct, Compose “just works”:
docker compose up -d
docker compose ps
If you see socket permission errors, jump to the socket troubleshooting section below.
Networking in Rootless Mode
Port publishing: why low ports fail
In Linux, binding to ports <1024 is privileged. Rootless Docker cannot bind directly to :80 or :443 without help.
Example failure:
docker run --rm -p 80:80 nginx
You may see errors like “permission denied” or “cannot bind”.
Fix: allow binding to privileged ports
Option A: Use a higher host port (simplest):
docker run --rm -p 8080:80 nginx
Option B: Allow unprivileged port binding via sysctl:
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0
Persist it:
echo 'net.ipv4.ip_unprivileged_port_start=0' | sudo tee /etc/sysctl.d/99-rootless-docker.conf
sudo sysctl --system
Now try:
docker run --rm -p 80:80 nginx
Option C: Use a reverse proxy on the host (rootful service) forwarding to a high port. This keeps the kernel default intact.
Host networking and why it’s different
--network host is fundamentally a “share host namespace” feature. In rootless mode, networking is already user-space mediated; host networking may not behave identically or may be unsupported depending on your setup.
Prefer bridge networking and explicit port publishing:
docker run --rm -p 127.0.0.1:8080:80 nginx
Binding to 127.0.0.1 is often desirable on developer machines.
Storage & Filesystem Behavior
Where rootless Docker stores data
Rootful Docker stores data under /var/lib/docker. Rootless Docker stores under your home/user directories, commonly:
~/.local/share/docker- runtime socket under
/run/user/<uid>/docker.sock
Inspect:
docker info | grep -E 'Docker Root Dir|Storage Driver'
You can also see disk usage:
docker system df
du -sh ~/.local/share/docker 2>/dev/null || true
OverlayFS constraints and fuse-overlayfs
Rootless can’t always use kernel overlay2 due to permission constraints. fuse-overlayfs provides an overlay filesystem in user space.
Check the driver:
docker info | grep -i 'Storage Driver'
If you see vfs, performance may be poor. Installing fuse-overlayfs and ensuring it’s available often improves this.
Fixing Common Permission Errors (with Real Causes)
This section is the practical core: error messages, what they mean in rootless mode, and how to fix them.
permission denied on the Docker socket
Symptoms:
docker ps
# Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
Cause: Your client is trying to use the rootful socket (/var/run/docker.sock) but you’re not in the docker group (or root). In rootless mode you should be using your user socket.
Fix 1: Point to the rootless socket:
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker ps
Fix 2: Use contexts:
docker context use rootless
docker ps
Fix 3: Confirm the rootless daemon is running:
systemctl --user status docker
systemctl --user start docker
Also confirm the socket exists:
ls -l /run/user/$(id -u)/docker.sock
If the socket exists but permissions look wrong, restart the user service:
systemctl --user restart docker
cannot create /var/run/docker.sock: no such file or directory
Symptoms appear when scripts hardcode /var/run/docker.sock.
Cause: Rootless Docker does not create /var/run/docker.sock.
Fix: Update scripts/CI to use the rootless socket:
- set
DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock - or mount the correct socket into containers that need Docker access (Docker-outside-of-Docker pattern)
Example: run a container that uses the host Docker daemon (rootless):
docker run --rm -it \
-e DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock \
-v /run/user/$(id -u)/docker.sock:/run/user/$(id -u)/docker.sock \
alpine sh
Inside that container, you’d also need the Docker CLI binary if you want to run docker commands.
newuidmap / newgidmap errors
Common messages:
newuidmap: write to uid_map failed: Operation not permittedcannot find newuidmap/newgidmapNo subuid ranges found for user
Causes and fixes:
- Missing
uidmaptools:
which newuidmap newgidmap
If not found, install:
- Debian/Ubuntu:
sudo apt-get install uidmap - Fedora:
sudo dnf install uidmap
- Missing subordinate ID ranges:
grep "^$USER:" /etc/subuid
grep "^$USER:" /etc/subgid
If empty, add:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"
Then log out/in and rerun:
dockerd-rootless-setuptool.sh install
- Kernel/user namespace restrictions (common in hardened environments)
Check:
sysctl kernel.unprivileged_userns_clone
If 0, rootless containers may be blocked. You can enable (if policy allows):
sudo sysctl -w kernel.unprivileged_userns_clone=1
echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf
sudo sysctl --system
Note: Some distros enable user namespaces by default; others restrict them for security policy reasons.
write /proc/...: permission denied and missing capabilities
Symptoms: a container tries to change kernel parameters, mount filesystems, or use privileged operations.
Examples:
- writing to
/proc/sys/... - mounting inside container
- changing network stack settings
Cause: Rootless containers have fewer effective capabilities on the host. Even if the container process is “root” inside its namespace, it’s not host-root.
Fix options:
- Redesign: avoid privileged operations in containers.
- Run that container in rootful Docker if it truly needs host-level privileges.
- Where possible, use application-level configuration instead of sysctls.
Example: if an image expects to run sysctl -w, consider setting sysctls at the host level (if appropriate) or using a rootful environment.
Bind mounts: permission denied / wrong ownership inside container
This is one of the most confusing rootless issues.
Symptom A: Container can’t write to a bind mount
Example:
mkdir -p ~/appdata
chmod 700 ~/appdata
docker run --rm -it -v ~/appdata:/data alpine sh -lc 'touch /data/x'
# touch: /data/x: Permission denied
Cause: User namespace mapping means container “root” is not host root; file permissions on the host still apply. If the host directory is not writable by your user (or by the mapped UID), writes fail.
Fix: Make the directory writable by your actual user:
chmod u+rwx ~/appdata
Or use a directory you own with appropriate permissions:
mkdir -p ~/containers/appdata
chown "$USER:$USER" ~/containers/appdata
chmod 755 ~/containers/appdata
Symptom B: Files created by container appear with strange UID/GID on host
You might see files owned by 100000 or similar.
Cause: That’s the subordinate ID mapping at work.
Fix options:
- Accept it (it’s normal for rootless).
- Run processes inside container as your host UID/GID to make ownership intuitive.
Example:
docker run --rm -it \
-u $(id -u):$(id -g) \
-v "$PWD":/work \
-w /work \
alpine sh -lc 'touch created_by_me'
ls -ln created_by_me
This is a strong default for developer workflows.
Symptom C: You need “real root” ownership semantics
If a workload requires writing files as host root, rootless Docker is the wrong tool. Use rootful Docker, a VM, or adjust the workflow.
Volumes and SELinux/AppArmor gotchas
On SELinux systems, bind mounts may fail even when UNIX permissions look correct.
Symptoms can look like:
permission deniedon mounted paths- container can’t read/write despite
chmod 777
Fix: Apply SELinux labels on bind mounts (rootful Docker often uses :Z/:z). Rootless support varies by distro and configuration, but you can try:
docker run --rm -it -v "$PWD":/work:Z alpine sh
If labeling isn’t possible in your environment, prefer Docker-managed volumes (which live under your rootless Docker data dir) rather than arbitrary host paths.
AppArmor can also restrict operations; check dmesg/audit logs if you suspect MAC policy interference.
“Operation not permitted” when using ping, iptables, or raw sockets
Rootless containers generally cannot:
- manipulate iptables on the host
- create raw sockets (needed for
pingin some configurations) - load kernel modules
- use many privileged kernel interfaces
Example:
docker run --rm alpine sh -lc 'apk add --no-cache iputils && ping -c1 1.1.1.1'
# ping: permission denied (are you root?)
# or: Operation not permitted
Fix options:
- Use TCP-based health checks (curl) instead of ICMP ping.
- If you control the host, you can grant specific capabilities in some cases, but rootless mode still won’t magically become host-root.
- For advanced networking tools, consider rootful Docker or a VM.
Systemd user service issues
If Docker works only when you’re logged in, or the daemon stops after logout:
Check:
loginctl show-user "$USER" | grep -i linger
systemctl --user status docker
Enable lingering:
sudo loginctl enable-linger "$USER"
If the user service can’t connect to the session bus, ensure dbus-user-session is installed (Debian/Ubuntu) and that your systemd user instance is functional.
Security Notes and Trade-offs
Rootless Docker improves safety, but it’s not “perfect isolation”:
- User namespaces reduce privilege, but kernel vulnerabilities can still matter.
- Some hardening features differ from rootful mode (e.g., cgroups handling, networking implementation).
- Exposing the Docker socket (even rootless) still grants powerful control over containers and your files accessible via mounts.
Practical guidance:
- Treat the rootless Docker socket like a sensitive credential.
- Avoid mounting your entire home directory into untrusted containers.
- Prefer running containers as a non-root user (
-u $(id -u):$(id -g)) for predictable file ownership and reduced in-container privilege.
Troubleshooting Checklist
Run these commands and interpret results:
- Which daemon am I talking to?
docker context show
echo "$DOCKER_HOST"
docker info | sed -n '1,40p'
- Is the rootless daemon running?
systemctl --user status docker
ps aux | grep -E 'dockerd|rootless' | grep -v grep
- Does the socket exist and is it accessible?
ls -l /run/user/$(id -u)/docker.sock
- Do I have subuid/subgid configured?
grep "^$USER:" /etc/subuid
grep "^$USER:" /etc/subgid
- Are user namespaces allowed?
sysctl kernel.unprivileged_userns_clone
- What storage driver is used?
docker info | grep -i 'Storage Driver'
- Port binding issues?
sysctl net.ipv4.ip_unprivileged_port_start
Uninstall / Reset Rootless Docker
To remove the user service and configuration:
dockerd-rootless-setuptool.sh uninstall
Stop and disable the user service:
systemctl --user disable --now docker
Remove rootless Docker data (this deletes images/containers/volumes for rootless daemon):
rm -rf ~/.local/share/docker
rm -rf ~/.docker
If you set environment variables in ~/.bashrc, remove them:
grep -n 'DOCKER_HOST=unix:///run/user' ~/.bashrc
# edit file and remove the line
Summary: When Rootless Docker Is the Right Choice
Rootless Docker is ideal when:
- you can’t (or shouldn’t) run a root daemon
- you want safer local development
- you’re on shared infrastructure with limited privileges
- you can accept networking/storage differences and avoid privileged container workloads
If you routinely need:
--privileged- kernel module access
- host networking parity
- iptables manipulation
- low-level observability tools requiring raw sockets
…then rootful Docker or a VM-based approach will be more compatible.
If you share a specific error message and your distro/kernel version, I can map it to the exact rootless limitation and provide a targeted fix (including which socket path and which sysctls/packages you need).