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 info includes the name=rootless security option.
  • The id command inside the container shows uid=0(root), so it is root from the perspective of programs in the container.
  • From the host (outside the container), the same sleep 300 process is owned by the regular user rootless.

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/shadow directly.
  • Rootless Docker: The process is still root inside the container (id -u is 0), but reading the file returns Permission 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

LimitationDescription and solution
Ports below 1024Regular 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 performanceTraffic is forwarded through user-space networking components, so throughput is lower than in regular mode
Data locationImages and Volumes are stored in the user's home directory (~/.local/share/docker) and are not shared with regular mode
Some featuresFor example, AppArmor settings and some storage and network drivers are unsupported

The test environment for this tutorial has ip_unprivileged_port_start adjusted, so -p 80:80 did 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 run works like Docker, and options such as --rm, -d, and -p work directly.
  • podman build uses 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 as alpine:3.23) use system configuration to determine which Registry to download from, and settings can vary by distribution.
  • Finally, switch to the regular user podman to run it: podman info shows rootless as true. 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 DockerPodman
Daemondockerd (root)dockerd (regular user)None
Actual privileges of container rootHost root (restricted by isolation)Regular userRegular user (when rootless)
Compatibility with commands in this tutorialIdenticalAlmost identicalMostly identical
Docker ComposeBuilt inBuilt inpodman compose calls an external implementation
Ecosystem tools (Portainer, Traefik, etc.)Fully supportedMostly supportedRequires additional socket configuration
Best forGeneral development, Docker Desktop, most serversMulti-user servers, high security requirementsRHEL / 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
  1. 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.
  2. 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.conf to 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