Docker’s default installation runs the daemon as root. Every container
you start inherits that privilege model — the container runtime
(containerd, runc) operates with full root capabilities on the
host. A single container escape, even from a well-configured
container, means the attacker has root on your machine.
Rootless mode changes this. The Docker daemon runs under an unprivileged user, and all containers use the user’s UID namespace mapping. Container process UID 0 maps to an unprivileged host UID — typically in the 100000-165536 range. Even a full break-out from the container yields only that unprivileged user’s permissions.
This guide covers everything you need to run Docker rootless in a homelab: installation, networking, Docker Compose, systemd integration, Proxmox LXC compatibility, and the real-world trade-offs you need to understand before switching.
Why Rootless Matters — The Privilege Problem
A standard Docker installation creates these attack vectors:
-
Daemon socket:
/var/run/docker.sockowned byroot:docker. Any user in thedockergroup has effective root access — they can mount the host filesystem, spawn privileged containers, and escape the namespace model entirely. -
Container break-out: A kernel exploit inside a container running as root (UID 0 in the container namespace) gives the attacker root on the host because the container’s UID 0 is the host’s UID 0.
-
Nested containerization: Running Docker inside an LXC container on Proxmox typically requires a privileged container. If Docker runs as root inside a privileged LXC, the LXC’s root is the host’s root — one layer of isolation gone.
Rootless mode solves all three:
- The daemon runs as a non-root user — no root-owned socket.
- Container UID 0 maps to an unprivileged UID on the host.
- You can run Docker inside an unprivileged LXC container with no special permissions.
How Rootless Mode Works
Rootless mode uses several Linux kernel features working together:
┌────────────────────────────────────────┐
│ Host (root user) │
│ ┌──────────────────────────────────┐ │
│ │ dockerd (user: dockeruser) │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ rootlesskit │ │ │
│ │ │ ┌──────────────────────┐ │ │ │
│ │ │ │ containerd │ │ │ │
│ │ │ │ ┌────────────────┐ │ │ │ │
│ │ │ │ │ runc │ │ │ │ │
│ │ │ │ │ (newuidmap) │ │ │ │ │
│ │ │ │ │ UID: 100001 │ │ │ │ │
│ │ │ │ └────────────────┘ │ │ │ │
│ │ │ └──────────────────────┘ │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
- rootlesskit — Reimplements network, mount, and PID namespaces
without root. It uses
newuidmap/newgidmapto create user namespaces with sub-UID/GID ranges. - slirp4netns or pasta — Provides network connectivity from the user namespace to the host network without root. Traffic exits through a TAP device in a separate network namespace.
- sub-UID/GID ranges — The Linux kernel’s user namespace support
maps container UID 0 to an unprivileged host UID (e.g., 100000).
/etc/subuidand/etc/subgiddefine these ranges.
The result: every containerized process runs under an unprivileged
UID on the host. ps aux inside the container shows UID 0, but
ps aux on the host shows the mapped unprivileged UID.
Prerequisites
Rootless mode requires specific kernel features and tools:
Kernel Requirements
|
|
Required Packages
|
|
The uidmap package provides newuidmap and newgidmap — the
setuid helpers that create user namespaces. Without them, rootless
mode cannot map UIDs.
Docker Version
|
|
Installation — Rootless Mode
Do NOT install Docker rootless via apt. The distro package installs
a rootful daemon. Use Docker’s official rootless installer instead.
Method 1: Clean Install (No Existing Docker)
|
|
The installer:
- Downloads the Docker static binaries
- Sets up
~/.config/systemd/user/docker.service - Configures environment in
~/.bashrc - Starts the rootless daemon via systemd –user
After install, log out and back in, or source the env:
|
|
Method 2: Alongside Rootful Docker
If you already have rootful Docker and want both running:
|
|
Method 3: Migrate from Rootful
This is the most common scenario for homelabs:
|
|
Post-Installation Setup and Validation
Verify Rootless Status
|
|
Environment Setup
The rootless installer adds this to ~/.bashrc. Verify it:
|
|
If SSH-ing into the machine, ensure XDG_RUNTIME_DIR is set:
|
|
Add it to ~/.profile for SSH sessions:
|
|
Networking — The Biggest Difference
This is where rootless mode differs most from rootful Docker. In
rootless mode, containers cannot directly use host networking
(--network host) or expose ports on privileged ports (< 1024).
Default Network: slirp4netns
Rootless containers use slirp4netns by default, which creates a
userspace TCP/IP stack:
|
|
Limitations:
- No ICMP (no
pinginside containers) - No
--network host - No multicast or broadcast
- Throughput is lower than bridge networking
- Connection tracking is per-connection (higher latency)
Pasta — The slirp4netns Replacement
Docker 25+ supports pasta (from the passt project), which performs
better than slirp4netns:
|
|
Pasta provides:
- Near-native throughput (~95%)
- Better port forwarding performance
- Reduced CPU overhead
- Still no
--network hostor ICMP
Benchmark comparison:
|
|
Exposing Ports Below 1024
By default, rootless cannot bind to ports below 1024. Three workarounds:
A) authbind — Grant a user permission to bind low ports:
|
|
Then configure Docker to use authbind (requires patching the rootlesskit start script — not recommended).
B) Proxy redirect — iptables REDIRECT on the host (recommended):
|
|
Add to a systemd oneshot service for persistence:
|
|
|
|
C) Reverse proxy with CAP_NET_BIND_SERVICE:
Run a reverse proxy (Caddy, Traefik, Nginx) in a rootful Docker with
only CAP_NET_BIND_SERVICE to bind low ports, then proxy to rootless
containers on high ports:
|
|
Option B (iptables redirect) is the simplest for homelabs and does not require any additional containers.
Docker Compose with Rootless
Docker Compose works out of the box with rootless mode. No special configuration needed.
|
|
Compose Rootful vs Rootless Differences
| Feature | Rootful | Rootless |
|---|---|---|
| Port bindings < 1024 | Yes | No (use redirect) |
network_mode: host |
Yes | No |
volumes with host paths |
Full access | User-owned paths only |
privileged: true |
Yes | Ignored (no effect) |
cap_add |
All capabilities | Limited to user-ns-safe caps |
Volume Mounts — User Permission Model
Rootless Docker can only mount directories the user owns or has
permission to read. If your compose file references /var/log or
/etc/nginx, those mounts will fail:
|
|
Fix by copying files into your home directory or creating a symlink farm with bind mounts from a rootful service.
Systemd Integration — User Services
Rootless Docker runs as a systemd user service. This is critical for homelab uptime — it must survive reboots and SSH session closures.
Enable linger for the Docker user
|
|
Verify the docker service starts at boot
|
|
Running Custom Containers as Systemd User Services
Each container can run as its own systemd unit with automatic restart:
|
|
Running Rootless Docker Inside Proxmox LXC
This is where rootless mode shines for homelabs. Instead of creating a full VM for Docker (which wastes RAM on a kernel and init), you can run rootless Docker inside an unprivileged LXC container.
LXC Container Configuration
Create the container on Proxmox with these settings:
|
|
Inside the LXC — Install Rootless Docker
|
|
Important: Sub-UID/GID Ranges Inside LXC
Unprivileged LXC containers have their own UID mapping. The container itself is mapped to a sub-UID range on the Proxmox host (e.g., UID 100000 on the host = UID 0 inside the container). Nested Docker rootless maps AGAIN from that range.
This means a container process has these UID translations:
Docker container UID 0
→ LXC container UID 100000 (via LXC's subuid mapping)
→ Proxmox host UID 101000 (typical nested mapping)
This triple-nesting is safe and works reliably when:
- The LXC container is unprivileged
/etc/subuidand/etc/subgidinside the LXC have enough range (65536 minimum)kernel.keys.maxkeysis set high enough inside the LXC
|
|
Monitoring and Maintenance
Logging
Rootless Docker logs go to the user’s systemd journal:
|
|
Updates
Update the rootless binaries when Docker releases a new version:
|
|
Backup
The rootless Docker data directory is at ~/.local/share/docker/:
|
|
Common Issues and Fixes
“Failed to connect to bus” or “XDG_RUNTIME_DIR not set”
|
|
Add both to ~/.profile for persistence.
“newuidmap: write to uid_map failed: Operation not permitted”
The sub-UID/GID ranges are missing or misconfigured:
|
|
“overlayfs: missing upperdir” or overlay not supported
Rootless Docker prefers fuse-overlayfs when the kernel’s overlay
in user namespace is not available:
|
|
Container Cannot Write to Mounted Host Directory
The container UID inside the user namespace (e.g., UID 100001 on host) must own the host directory:
|
|
Or use podman-style subuid mount options (Docker 25+):
|
|
Trade-offs — When Not to Use Rootless
Rootless mode is not always the right choice:
| Scenario | Recommendation |
|---|---|
| Single-user homelab, no untrusted services | Rootless is fine, adds defense-in-depth |
| Running Docker in an LXC on Proxmox | Rootless strongly recommended — avoids privileged LXC |
Need --network host for performance |
Rootless cannot do this. Use rootful or use pasta |
| Need ICMP/ping inside containers | Rootless cannot provide this |
| Services must bind port 80/443 directly | Use iptables redirect or reverse proxy |
| GPU passthrough to containers | Rootless adds complexity with /dev/dri access |
| Full production with strict SLAs | Evaluate carefully — rootless has higher overhead |
For most homelab use cases — where you run a reverse proxy on one port and web services on high ports behind it — the trade-offs are minimal. The security benefit of removing root from the container runtime is worth the networking compromises.
Summary
Docker rootless mode eliminates the largest security risk of
containerization — the root-privileged daemon and the docker group
backdoor. The installation is a single command:
|
|
Key takeaways:
- Security: Daemon and containers run under an unprivileged UID. Container break-outs yield a non-root user, not host root.
- Networking: Use pasta for near-native throughput. Redirect
privileged ports with iptables. Accept the loss of
--network hostand ICMP. - Proxmox: Rootless Docker inside an unprivileged LXC is the most resource-efficient and secure Docker hosting pattern on Proxmox.
- Systemd: Enable linger, verify the user service autostarts, and optionally wrap critical containers as systemd user services.
Rootless mode converts Docker from “root with extra steps” to a genuinely sandboxed container runtime. For a homelab where security matters — and it should — the transition takes ten minutes and pays dividends in reduced attack surface.