Podman Rootless Containers: The Secure Alternative to Docker

By Eddie Power Feb 4, 2026 in Containers & Virtualization 6 min read
bash podman ssh

Docker's default setup runs a daemon as root, and anyone in the docker group can effectively become root on the host. For a lot of self-hosted services that's more privilege than you need. Podman takes a different approach: no central daemon, and containers that run as your normal user by default. If a process breaks out of a rootless container, it lands as an unprivileged user, not root.

This post covers how rootless Podman works, how to set it up, and the parts that behave differently from Docker. If you're coming from Docker, my post on Docker in Production covers volumes, networks and secrets. Most of those ideas carry straight over.

How rootless containers work

Rootless Podman relies on user namespaces. Inside the container, a process can be UID 0 ("root"), but the kernel maps that to your own UID on the host. Other UIDs inside the container are mapped to a range of subordinate IDs reserved for your user in /etc/subuid and /etc/subgid.

You can see the mapping directly:

$ podman unshare cat /proc/self/uid_map
         0       1000          1
         1     100000      65536

Read that as: container UID 0 is host UID 1000 (you), and container UIDs 1–65536 map to host UIDs 100000–165535. Nothing in the container ever runs as real root on the host.

Installing Podman

On Debian, Ubuntu and Fedora it's in the standard repos:

sudo apt install podman uidmap slirp4netns   # Debian/Ubuntu
sudo dnf install podman                      # Fedora/RHEL

Recent distros add subordinate ID ranges automatically when a user is created. Check yours:

grep "^$USER:" /etc/subuid /etc/subgid

If nothing comes back, add a range (this needs root once) and tell Podman to pick it up:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
podman system migrate

Then confirm you're really running rootless:

podman info --format '{{.Host.Security.Rootless}}'
podman run --rm docker.io/library/alpine id

The first command should print true. The second shows uid=0(root) *inside* the container, which is the user namespace doing its job.

Day-to-day commands

The CLI is deliberately Docker-compatible, so run, ps, logs, exec, build and pull all work the same way. Some people simply alias docker=podman. A quick web server:

podman run -d --name web -p 8080:80 docker.io/library/nginx:alpine
curl -I http://127.0.0.1:8080/
podman top web user huser

podman top with the huser column shows the host user behind each container process. The nginx worker that thinks it's nginx is really one of your subordinate UIDs.

One habit worth adopting: use fully qualified image names (docker.io/library/nginx), not just nginx. Podman can search several registries, and spelling out the registry avoids pulling something unexpected.

Things that behave differently

Ports below 1024

An unprivileged user can't bind to ports under 1024 by default, so -p 80:80 fails rootless. You have three options:

  1. Publish on a high port (-p 8080:80) and put a reverse proxy or a firewall redirect in front.
  2. Lower the limit system-wide with net.ipv4.ip_unprivileged_port_start=80 in /etc/sysctl.d/. That's simple, but it applies to every user.
  3. Run that one container rootful with sudo podman.

For a home server I usually go with option 1: a reverse proxy listening on 80/443 forwards to containers on high ports.

Volume permissions

Because of the UID mapping, files a container writes into a bind mount are owned by subordinate UIDs on the host, which can look odd in ls -l. Two tools help:

  • podman unshare chown -R 33:33 ./data changes ownership *as seen from inside* the namespace, so the container's UID 33 owns it.
  • --userns=keep-id maps your host UID to the same UID inside the container, which is handy for dev containers that edit your source tree.

On Fedora and RHEL with SELinux, add :Z (private) or :z (shared) to bind mounts so they get the right label: -v ./site:/usr/share/nginx/html:ro,Z.

Networking

Rootless containers use a user-mode network stack (pasta in Podman 5, slirp4netns in older releases) instead of a root-owned bridge. It's slightly slower, but for web apps and home services you won't notice. Containers that need to talk to each other should share a pod or a user-defined network:

podman network create appnet
podman run -d --name db  --network appnet -e POSTGRES_PASSWORD=change-me docker.io/library/postgres:16
podman run -d --name app --network appnet -p 8080:8080 my-app:latest

Containers on the same network resolve each other by name, just as with Docker Compose.

Compose files

You don't have to rewrite your compose.yaml. You have two options:

  • podman compose up -d calls an external Compose provider (podman-compose or docker-compose) under the hood.
  • Or enable the Docker-compatible API socket and point the real Docker Compose at it:
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker compose up -d

Starting containers at boot with Quadlet

There's no daemon to restart your containers, so Podman leans on systemd instead. The current way to do this is Quadlet: you write a small .container file and systemd generates a full service from it. (podman generate systemd still exists but is deprecated.) If systemd units are new to you, Systemd Units Explained covers the basics.

Create ~/.config/containers/systemd/web.container:

[Unit]
Description=Nginx (rootless Podman)

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80
Volume=%h/sites/default:/usr/share/nginx/html:ro,Z
AutoUpdate=registry

[Service]
Restart=on-failure

[Install]
WantedBy=default.target

Then load and start it:

systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service

User services normally stop when you log out. To keep them running and start them at boot without a login, enable lingering once:

sudo loginctl enable-linger $USER

AutoUpdate=registry labels the container so podman auto-update can pull a newer image and restart the service. Run it by hand, or enable the bundled podman-auto-update.timer.

When rootless isn't the right fit

Rootless doesn't suit everything. Containers that need to manage host networking (a VPN gateway, for example; see my WireGuard guide for doing that on the host instead), access raw devices, or bind low ports on the host network may be simpler to run rootful. Podman handles that too with sudo podman, and rootful and rootless containers live side by side with separate image stores.

Conclusion

Rootless Podman gives you nearly the same workflow as Docker, with one big difference: a compromised container no longer means a compromised host. Start with one low-risk service, get comfortable with the port and volume differences, then move it to a Quadlet unit so it survives reboots.

If you'd rather have someone set up a container host, a web server or a new website for your business, I do that kind of work. Get in touch through my portfolio.

Share this article

E
Eddie Power admin

Linux enthusiast and open source advocate.

Comments (0)

No comments yet. Be the first to leave one!

Leave a Comment

Comments are moderated and will appear after approval.