Contents
Rootless Docker and Podman Tutorial: Run Containers Without Root
Security best practices showed that anyone who can use Docker can mount the host's root directory in a container and read or write any file, which is equivalent to having root privileges on the host. That's because Docker Engine (dockerd) runs as root, so root inside the container is host root (limited only by various isolation mechanisms).
This section introduces two ways to avoid running as root: Docker's Rootless mode and another container tool, Podman.
Rootless Docker: Docker Engine runs as a regular user
Rootless mode runs dockerd and all containers as a regular user. It uses Linux user namespaces: a process appears as root (uid 0) inside the container, but on the host it maps to your account or a range of unprivileged UIDs.
This section tests the official docker:29.9-dind-rootless image without changing the host configuration:
$ docker run -d --name rl --privileged docker:29.9-dind-rootless
93e609c8808360d7e63bdc214ab2ff0e76af520fb0fb9dec5b5852d57a4881fe
$ docker logs rl 2>&1 | tail -n 5
time="2026-10-13T05:22:59.775914576Z" level=info msg="Initializing buildkit"
time="2026-10-13T05:22:59.918099929Z" level=info msg="Completed buildkit initialization"
time="2026-10-13T05:22:59.929895038Z" level=info msg="Daemon has completed initialization"
time="2026-10-13T05:22:59.930093633Z" level=info msg="API listen on /run/user/1000/docker.sock"
time="2026-10-13T05:22:59.930231753Z" level=info msg="API listen on [::]:2376"
$ docker exec rl id
uid=1000(rootless) gid=1000(rootless) groups=1000(rootless)
The logs show that Docker Engine listens on /run/user/1000/docker.sock (a path dedicated to user 1000), rather than the regular /var/run/docker.sock. The user running dockerd is rootless (uid 1000), not root.
Who is root inside the container?
Point the docker command at the rootless socket (DOCKER_HOST) and run a container:
$ docker exec -e DOCKER_HOST=unix:///run/user/1000/docker.sock rl docker info --format '{{json .SecurityOptions}}'
["name=seccomp,profile=builtin","name=rootless"]
$ docker exec -e DOCKER_HOST=unix:///run/user/1000/docker.sock rl docker run -d --name sleeper alpine:3.23 sleep 300
18986d442e9b9282178d96e7187534b9ac19a83dd42e3b4671a5243b55bcf1ff
$ docker exec -e DOCKER_HOST=unix:///run/user/1000/docker.sock rl docker exec sleeper id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
$ docker exec rl ps -o user,pid,args | grep 'sleep 300' | grep -v grep
rootless 382 sleep 300
docker infoincludes thename=rootlesssecurity option.- The
idcommand inside the container showsuid=0(root), so it is root from the perspective of programs in the container. - From the host (outside the container), the same
sleep 300process is owned by the regular userrootless.
Test: mounting the host root directory still can't read /etc/shadow
/etc/shadow stores password hashes and only root can read it. Mount the host's / and read the file with regular Docker and Rootless Docker:
$ docker run --rm -v /:/host alpine:3.23 sh -c "id -u; head -n 1 /host/etc/shadow"
0
root:*::0:::::
$ docker exec -e DOCKER_HOST=unix:///run/user/1000/docker.sock rl docker run --rm -v /:/host alpine:3.23 sh -c 'id -u; head -n 1 /host/etc/shadow'
0
head: /host/etc/shadow: Permission denied
- Regular Docker: root inside the container can read the host's
/etc/shadowdirectly. - Rootless Docker: The process is still root inside the container (
id -uis 0), but reading the file returnsPermission denied. On the host, it is only a regular user, so it cannot have more privileges than the account that started Docker.
So even if a container is compromised or someone accidentally uses -v /:/host, the impact is limited to that user account.
Install Rootless Docker on Linux
The steps below are based on the official Docker documentation. This tutorial does not install it on the host (installation changes system configuration); follow the official documentation when you do it yourself.
# 1. Install the required packages (Ubuntu / Debian)
sudo apt-get install -y uidmap dbus-user-session
# 2. If regular Docker is already installed, it is recommended that you disable it first
sudo systemctl disable --now docker.service docker.socket
# 3. Run the setup script as a regular user (installed with the Docker Engine package)
dockerd-rootless-setuptool.sh install
# 4. Point the docker command at the rootless socket (add this to ~/.bashrc)
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# 5. Start it at boot (keep it running after the user logs out)
systemctl --user enable docker
sudo loginctl enable-linger $(whoami)
Rootless limitations
| Limitation | Description and solution |
|---|---|
| Ports below 1024 | Regular users cannot bind to 80 or 443 by default, so -p 80:80 fails. Use a port such as 8080 and forward traffic through a reverse proxy, or change the system setting net.ipv4.ip_unprivileged_port_start |
| Resource limits | --memory and --cpus require cgroup v2 on the host and permissions delegated to the user by systemd |
| Network performance | Traffic is forwarded through user-space networking components, so throughput is lower than in regular mode |
| Data location | Images and Volumes are stored in the user's home directory (~/.local/share/docker) and are not shared with regular mode |
| Some features | For example, AppArmor settings and some storage and network drivers are unsupported |
The test environment for this tutorial has
ip_unprivileged_port_startadjusted, so-p 80:80did not fail. The first limitation in the table cannot be reproduced here.
Podman: a container tool with no daemon
Podman is an open-source container tool led by Red Hat and is the default container tool in distributions such as Fedora and RHEL. Its biggest differences from Docker are:
- No daemon (daemonless): Docker commands run through dockerd; Podman commands create containers directly, and the containers are child processes of your process.
- Rootless by default: When a regular user runs
podman, it runs in rootless mode. - Commands are almost identical to Docker: Options for
run,build,ps,images,exec,logs, and more are the same.
Test: use Podman the Docker way
Run it in the quay.io/podman/stable image (the test host's cgroup v1 causes Podman to show a deprecation warning, so PODMAN_IGNORE_CGROUPSV1_WARNING=1 hides it):
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman podman --version
podman version 5.8.7
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman podman run --rm docker.io/library/alpine:3.23 echo hello from podman
hello from podman
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman podman run --rm alpine:3.23 echo hi
hi
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman podman build -q -t express-app /tmp/express-app
npm notice
npm notice New major version of npm available! 11.19.0 -> 12.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0
npm notice To update run: npm install -g npm@12.2.0
npm notice
npm warn using --force Recommended protections disabled.
a5a0b2b95ea756ad73b92ead62ece1770fdb26033c69749933fe03e3988dcb8d
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman podman run -d --name app -p 3000:3000 express-app
74aeae727d68aacae32aee45d0705881fbea9b449ac1a5ee07c365b1490f42b3
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman podman exec app wget -qO- localhost:3000
{"message":"Hello from Express","node":"v24.21.0"}
$ docker exec -e PODMAN_IGNORE_CGROUPSV1_WARNING=1 podman su podman -c 'podman run --rm docker.io/library/alpine:3.23 id -u; podman info --format {{.Host.Security.Rootless}}'
0
true
podman runworks like Docker, and options such as--rm,-d, and-pwork directly.podman builduses the Dockerfile from the Node.js application directly and builds successfully without any changes; the container also responds as expected.- It's best to use the full image name, such as
docker.io/library/alpine:3.23. Podman's short names (such asalpine:3.23) use system configuration to determine which Registry to download from, and settings can vary by distribution. - Finally, switch to the regular user
podmanto run it:podman infoshowsrootlessastrue. The container still has uid 0, but just as with Rootless Docker, it maps to a regular user on the host.
Install
# Ubuntu / Debian
sudo apt-get install -y podman
# Fedora / RHEL
sudo dnf install -y podman
On Windows and macOS, you can install Podman Desktop, which runs Linux in a virtual machine just like Docker Desktop.
Choosing between Docker, Rootless Docker, and Podman
| Docker (regular) | Rootless Docker | Podman | |
|---|---|---|---|
| Daemon | dockerd (root) | dockerd (regular user) | None |
| Actual privileges of container root | Host root (restricted by isolation) | Regular user | Regular user (when rootless) |
| Compatibility with commands in this tutorial | Identical | Almost identical | Mostly identical |
| Docker Compose | Built in | Built in | podman compose calls an external implementation |
| Ecosystem tools (Portainer, Traefik, etc.) | Fully supported | Mostly supported | Requires additional socket configuration |
| Best for | General development, Docker Desktop, most servers | Multi-user servers, high security requirements | RHEL / Fedora environments, avoiding a daemon |
For most beginners and regular projects, you can use Docker as shown in this tutorial. Consider Rootless Docker or Podman if a server has multiple users or higher security requirements.
Recap
- Regular Docker's dockerd runs as root, so being able to use Docker is equivalent to having root on the host.
- Rootless mode uses user namespaces to map root inside the container to a regular user on the host. Testing confirmed it cannot read the host's
/etc/shadow. - The main Rootless limitations are ports below 1024, resource limits that require cgroup v2, and network performance.
- Podman has no daemon, is rootless by default, and has commands that are almost identical to Docker. It can build Dockerfiles directly.
Exercises
Exercise 1: In regular Docker, run docker run --rm alpine:3.23 id, then run docker run --rm -u 1000:1000 alpine:3.23 id. How is this different from Rootless mode?
Answer
-u 1000:1000 only runs the process inside the container as uid 1000; dockerd still runs as root, and anyone who can use Docker can start another root container without -u. In Rootless mode, the entire Docker Engine runs as a regular user, so no container can get root privileges on the host. You can use both modes at the same time.
Exercise 2: Running docker run -d -p 80:80 nginx:1.30-alpine in Rootless Docker fails. What are two ways to fix it?
Answer
- Use a port above 1024, such as
-p 8080:80, then use a host reverse proxy or firewall rule to forward port 80 to 8080. - Change the system setting to let regular users bind to port 80:
sudo sysctl net.ipv4.ip_unprivileged_port_start=80(write it to/etc/sysctl.confto keep it after reboot).
FAQ
Is Rootless mode slower?
Programs running inside containers perform about the same. Networking (through user-space forwarding) and some filesystem operations are slower than in regular mode, but most websites and development workloads feel no difference.
Does Docker Desktop need Rootless mode?
No. Docker Desktop runs Docker Engine in a virtual machine on Windows and macOS, separated from your computer. Rootless is mainly for Linux servers and desktops.
Can I replace the docker command directly with podman?
Mostly. The options for podman run, podman build, and podman ps are nearly the same, and many people simply set alias docker=podman. The differences are that Podman has no daemon, is rootless by default, and tools related to Compose and the Docker socket need additional configuration.
Can Podman use compose.yaml?
Yes. podman compose calls the Compose implementation installed on your system (docker-compose or podman-compose) to process compose.yaml. Most settings work.
Updated October 13, 2026