Rootless Podman: Run Containers Without Root

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
shadow-utils-4.15.0-11.el10.x86_64
shadow-utils-subid-4.15.0-11.el10.x86_64
Applies to Any Linux host with Podman installed
Privilege Normal user podtest for rootless examples; administrator access for /etc/subuid and /etc/subgid
Scope Rootless setup, subuid/subgid verification, newuidmap/newgidmap, uid_map inspection, first container, storage paths, XDG_RUNTIME_DIR, su vs su -, linger scope, podman system migrate, NFS/noexec storage, RHEL IdM/SSSD subID notes, and setup troubleshooting. Does not cover --userns modes, volume permission diagnosis, pasta internals, or privileged-port workarounds.
Related guides Fix Podman volume permission denied

Rootless Podman lets a normal Linux account run containers without executing the podman command as root. Setup failures usually trace back to subordinate UID/GID ranges, stale user namespaces, or a broken login environment — not missing container images. This guide walks through verification on the lab user podtest and the errors you are most likely to hit before the first successful podman run.


What is rootless Podman?

Rootless Podman runs the Podman process and containers under a normal Linux account instead of requiring the Podman CLI itself to run as root.

The core mechanism looks like this:

text
Host user
Rootless Podman user namespace
   ├── host user's UID → container/rootless namespace UID 0
   └── subordinate UIDs/GIDs → additional container identities

Root inside a rootless container is not host root. The container cannot gain privileges the user running Podman does not possess.

Rootless and rootful Podman also keep separate:

  • containers
  • images
  • volumes
  • configuration
  • storage

A container started with podman run ... as podtest does not appear in sudo podman ps, and the reverse is also true.


Check whether Podman is running rootless

Confirm the active context before changing subordinate ID files.

As the normal user:

bash
podman info --format '{{.Host.Security.Rootless}}'

On the lab account:

output
true

You can also spot the same flag in a full podman info transcript under the security section as rootless: true.

Establish the host identity next:

bash
id
output
uid=1014(podtest) gid=1015(podtest) groups=1015(podtest) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

When rootless: true and id shows your unprivileged account, you are exercising the rootless storage and namespace path.


Rootless Podman requirements

Before debugging obscure errors, confirm the baseline:

  • Podman installed — see Install Podman on RHEL, Ubuntu, Debian, or Rocky Linux
  • normal Linux login user, not only a shared service account invoked with broken environment inheritance
  • newuidmap and newgidmap on PATH
  • subordinate UID range in /etc/subuid
  • subordinate GID range in /etc/subgid
  • writable local storage for the user's graph root
  • supported rootless network helper such as pasta on Podman 5+
  • valid user runtime/session environment (HOME, XDG_RUNTIME_DIR when required)

Modern kernels can use native rootless OverlayFS. Rootless Podman does not always require fuse-overlayfs.


Check newuidmap and newgidmap

These helpers construct the subordinate UID/GID portions of the rootless user namespace.

Verify they exist:

bash
command -v newuidmap
output
/usr/bin/newuidmap

Check the GID mapper the same way:

bash
command -v newgidmap
output
/usr/bin/newgidmap

On RHEL-family systems they normally ship with shadow-utils. If either command is missing, install the distribution package that provides them before editing subordinate ID files.


Check /etc/subuid and /etc/subgid

Read the ranges assigned to the lab user:

bash
grep '^podtest:' /etc/subuid
output
podtest:983040:65536

Repeat for the GID file — the range should match:

bash
grep '^podtest:' /etc/subgid
output
podtest:983040:65536

The file format is:

text
USERNAME:START:COUNT

So podtest:983040:65536 means:

  • first subordinate ID = 983040
  • count = 65536
  • last subordinate ID = 1048575

That differs from usermod syntax, which uses a start-end range instead of a count.

Subordinate ranges must not overlap between users.


The usermod --add-subuids range trap

/etc/subuid uses START:COUNT, but usermod uses START-END.

To allocate the same 65536 IDs as the lab entry, an administrator would run:

bash
sudo usermod --add-subuids 983040-1048575 podtest

Allocate the GID range with the same start-end pair:

bash
sudo usermod --add-subgids 983040-1048575 podtest

The incorrect form --add-subuids 100000-65536 does not mean “start at 100000 with size 65536.” The second number is an ending ID, not a count.

After any change, verify the files:

bash
grep '^podtest:' /etc/subuid /etc/subgid
output
/etc/subuid:podtest:983040:65536
/etc/subgid:podtest:983040:65536

If ranges were added after Podman already initialized rootless state, continue to the podman system migrate section below.


Use getsubids to verify subordinate IDs

On current shadow-utils builds, getsubids is often clearer than reading the flat files alone:

bash
getsubids podtest
output
0: podtest 983040 65536

This becomes especially useful when subordinate IDs are not sourced directly from /etc/subuid, such as SSSD-backed ranges on IdM-managed hosts.


Inspect the actual rootless UID/GID mapping

File entries alone do not prove the active namespace is using them. Read the live map as podtest:

bash
podman unshare cat /proc/self/uid_map
output
0       1014          1
         1     983040      65536

Then the GID map:

bash
podman unshare cat /proc/self/gid_map
output
0       1015          1
         1     983040      65536

Interpretation:

  • namespace ID 0 maps to host UID 1014 (the podtest account)
  • namespace IDs 1 through 65536 map to host subordinate UIDs starting at 983040

That second line must exist for rootless image extraction and multi-entry container ID maps. Custom --uidmap tuning belongs in Podman user namespaces, not in this setup guide.


Run your first rootless container

Pull and run the upstream hello image:

bash
podman run --rm quay.io/podman/hello
output
!... Hello Podman World ...!

Run a shell-capable image and inspect IDs inside the container namespace:

bash
podman run --rm docker.io/library/alpine:3.20 id
output
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

uid=0(root) here does not mean host UID 0. Compare with the host-side unshare view:

bash
podman unshare id
output
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:container_runtime_t:s0-s0:c0.c1023

Both show namespace-local root mapped to the unprivileged host account.


Where rootless Podman stores its data

Rootless state is private to the user. Read the configured paths:

bash
podman info --format 'graphRoot={{.Store.GraphRoot}} runRoot={{.Store.RunRoot}} volumePath={{.Store.VolumePath}}'

On the lab account:

output
graphRoot=/home/podtest/.local/share/containers/storage runRoot=/run/user/1014/containers volumePath=/home/podtest/.local/share/containers/storage/volumes

Typical defaults when XDG_DATA_HOME is unset:

  • persistent storage: $HOME/.local/share/containers/storage
  • runtime state: $XDG_RUNTIME_DIR/containers (here /run/user/1014/containers)

Moving graphroot to local disk when $HOME is NFS-backed is covered in Podman storage location. This article does not walk through every storage.conf key.


What is XDG_RUNTIME_DIR?

XDG_RUNTIME_DIR holds user-specific runtime state such as the rootless runRoot directory. A broken value is a common setup failure when operators switch users incorrectly.

On a healthy systemd login session it is typically /run/user/$UID. Check ownership when troubleshooting:

bash
stat -c '%u %U %n' /run/user/1014
output
1014 podtest /run/user/1014

The directory owner must match the user running Podman. Do not tell readers to permanently hard-code export XDG_RUNTIME_DIR=/run/user/1014 in shell profiles as a universal fix. Fix the login session instead.


su vs su - with rootless Podman

Switching users incorrectly from root is a frequent source of XDG_RUNTIME_DIR errors.

A non-login su can inherit the previous user's environment:

bash
su podtest -c 'echo HOME=$HOME XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR'
output
HOME=/home/podtest XDG_RUNTIME_DIR=/run/user/0

/run/user/0 belongs to root, not podtest.

A login shell resets more of the environment:

bash
su - podtest -c 'echo HOME=$HOME XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR'
output
HOME=/home/podtest XDG_RUNTIME_DIR=

Even when XDG_RUNTIME_DIR is empty in a non-interactive su - snippet, Podman on the lab host still uses /run/user/1014/containers because that path exists for the user. The danger is inheriting another user's runtime directory, not an empty variable by itself.

Prefer SSH or su - with a full login session, and verify stat on the runtime directory before blaming subordinate IDs.


Fix XDG_RUNTIME_DIR is not owned by the current user

When Podman reports that error, compare the current UID with the runtime directory owner:

bash
id -u

Print the runtime directory Podman inherited from your session:

bash
echo "$XDG_RUNTIME_DIR"

Confirm which user owns that path:

bash
stat -c '%u %U %n' "$XDG_RUNTIME_DIR"

The UID column must match id -u. If you inherited /run/user/0 from a root su session, open a proper login session for podtest instead of recursively changing ownership under /run/user.

Do not delete /run/user/$UID as a standard repair. That directory is managed by the user's login session.


When loginctl enable-linger is required

loginctl enable-linger is not required merely to run podman run interactively in an SSH or desktop session.

Enable linger when:

  • user services must continue after logout
  • rootless Quadlet or systemd user units must run without an active login
  • rootless containers need to survive session termination
  • user services should start during boot without an interactive login

Enable it for the target account:

bash
sudo loginctl enable-linger podtest

Verify:

bash
loginctl show-user podtest -p Linger
output
Linger=yes

Logout and reboot behavior for long-running containers belongs in the container lifecycle guides. Rootless Quadlet units are covered separately.


Fix there might not be enough IDs available in the namespace

This error usually means the active rootless user namespace does not expose enough subordinate IDs, even when /etc/subuid looks correct on disk.

Inspect the live map first:

bash
podman unshare cat /proc/self/uid_map

A broken map might show only the single host-user line and omit the subordinate range starting at 983040.

Confirm the files:

bash
grep '^podtest:' /etc/subuid /etc/subgid

If ranges were added or changed after Podman initialized the rootless pause namespace, refresh Podman's persistent rootless state:

  1. stop the user's running containers
  2. run podman system migrate
  3. confirm /etc/subuid and /etc/subgid still contain the intended mappings
  4. start Podman again
  5. re-read uid_map and gid_map

Run the migrate step as the affected user:

bash
podman system migrate

podman system migrate exits silently on success on the lab host. Verify the map afterward:

bash
podman unshare cat /proc/self/uid_map
output
0       1014          1
         1     983040      65536

Do not jump straight to podman system reset for this error.


Why podman system migrate matters after subuid/subgid changes

Rootless Podman keeps a pause process alive to hold the user's unprivileged namespaces open. Editing /etc/subuid or /etc/subgid does not automatically rewrite a namespace that is already running.

podman system migrate stops the necessary rootless state so the next Podman operation can recreate the namespace with the updated subordinate ranges. That is why migrate belongs in the normal repair path and reset belongs only after deliberate data-loss review.


Fix newuidmap or newgidmap errors

When Podman reports failures involving newuidmap or newgidmap, verify the helpers and the user's ranges before changing permissions.

Check the binaries:

bash
ls -l "$(command -v newuidmap)"
output
-rwxr-xr-x. 1 root root 43144 Feb 23 05:30 /usr/bin/newuidmap

Inspect the GID helper permissions too:

bash
ls -l "$(command -v newgidmap)"
output
-rwxr-xr-x. 1 root root 43160 Feb 23 05:30 /usr/bin/newgidmap

Confirm subordinate ranges exist:

bash
getsubids podtest

Do not manually change helper capabilities unless distribution packaging is genuinely broken and official remediation supports it.


Whitespace in /etc/subuid or /etc/subgid

Trailing whitespace can prevent Podman from recognizing an otherwise valid entry. Inspect the raw file:

bash
cat -A /etc/subuid | grep '^podtest:'
output
podtest:983040:65536$

The line should end cleanly with $ and no stray spaces before the newline.


Rootless Podman and NFS home directories

Rootless Podman does not fail simply because $HOME lives on NFS. The problem is container storage on a filesystem that does not support the UID mapping semantics rootless storage needs.

If default graph root resolves to NFS:

text
$HOME/.local/share/containers/storage

container layers and metadata can fail in ways that look like permission errors.

Identify where $HOME is mounted:

bash
findmnt -T "$HOME"

On the lab host, $HOME is on local XFS:

output
TARGET SOURCE                FSTYPE OPTIONS
/      /dev/mapper/rhel-root xfs    rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota

When $HOME is remote, move rootless graphroot to local storage through storage.conf as described in Podman storage location. That is separate from bind-mounting application data from NFS into a container.


Home or storage mounted noexec / nodev

Rootless storage must live on a filesystem and mount combination that can execute and extract container content.

Check the mount options for the storage path:

bash
findmnt -T "$HOME"

A noexec home or graph root can produce permission denied when launching container processes. Point Podman storage at an appropriate local path instead of asking administrators to weaken corporate mount policy.


Rootless Podman with FreeIPA / RHEL IdM / SSSD subordinate IDs

Plain LDAP and RHEL IdM/SSSD subID integration are not the same thing. On current RHEL systems, centrally managed subordinate ranges can flow through SSSD when the platform is configured for it.

RHEL-oriented pieces include:

  • IdM assigning user subID ranges
  • shadow-utils-subid providing lookup support
  • nsswitch.conf delegating subid to one source
  • authselect features such as with-subid on supported releases

Example IdM-oriented enablement on RHEL:

bash
sudo authselect enable-feature with-subid

Verify delegation:

bash
getsubids idmuser

The subid nsswitch database supports one delegation source at a time, such as:

text
subid: files

or:

text
subid: sss

Do not maintain conflicting duplicate ranges in both files and SSSD views.

The lab host uses local files only and does not demonstrate IdM lookup. Treat the IdM path as an enterprise configuration note, not the default workstation workflow.


Rootless Podman configuration paths

Purpose Rootless location
containers config $HOME/.config/containers/containers.conf
storage config $HOME/.config/containers/storage.conf
registry config $HOME/.config/containers/registries.conf
persistent storage $HOME/.local/share/containers/storage by default
runtime state $XDG_RUNTIME_DIR/containers
registry auth $XDG_RUNTIME_DIR/containers/auth.json by default

Each file has its own dedicated guide when you need more than path awareness.


Do not use podman system reset as the first fix

podman system reset --force is destructive. It removes the user's local Podman state, including containers, pods, images, networks, and related storage metadata for that account.

Use it only when:

  • existing rootless storage is intentionally disposable
  • normal mapping and session repair have failed
  • you understand the data loss

Before considering reset, inspect what would disappear:

bash
podman ps -a

List local images that reset would delete:

bash
podman images

Include named volumes in the inventory:

bash
podman volume ls

Back up persistent data first. Do not present deleting ~/.config/containers or /run/user/$UID as a routine intermediate repair. Diagnose the specific failure instead.


Rootless Podman setup verification checklist

Work through these commands as the target user when rootless Podman fails before the first successful run.

Confirm identity:

bash
id

Confirm helpers:

bash
command -v newuidmap

The GID mapper should resolve the same way:

bash
command -v newgidmap

Confirm subordinate ranges:

bash
getsubids "$(id -un)"

Or read the files directly:

bash
grep "^$(id -un):" /etc/subuid /etc/subgid

Confirm the active namespace map:

bash
podman unshare cat /proc/self/uid_map

Confirm Podman sees rootless mode:

bash
podman info --format '{{.Host.Security.Rootless}}'

Finish with a real container:

bash
podman run --rm quay.io/podman/hello

When that hello container prints successfully and uid_map shows both the host user line and the subordinate range, rootless setup is in good shape. Networking behavior, volume permissions, and user-namespace modes are separate topics.


References


Summary

Rootless Podman runs under a normal Linux account through user namespaces and subordinate UID/GID ranges from /etc/subuid and /etc/subgid. The host user maps to namespace UID 0; additional container identities draw from the subordinate pool. That namespace-local root is not host root, and rootless storage is separate from anything created with sudo podman.

Setup debugging should follow a fixed order: confirm newuidmap and newgidmap, read subordinate ranges with getsubids, prove the live map with podman unshare cat /proc/self/uid_map, and run podman system migrate after administrators change ranges. Watch for the usermod start-end trap, inherited XDG_RUNTIME_DIR values from careless su, and NFS or noexec storage under the default graph root.

loginctl enable-linger matters for services that must outlive login sessions, not for a one-off interactive podman run. Reserve podman system reset --force for intentional wipes after you have listed containers, images, and volumes. Once hello-world succeeds, continue with Rootless Podman networking for pasta defaults and with Podman storage location if graph root must move off NFS.


Frequently Asked Questions

1. What is rootless Podman?

Rootless Podman runs the Podman CLI and containers under a normal Linux user account using user namespaces and subordinate UID/GID ranges. UID 0 inside a rootless container maps to the host user, not host root. Rootless and rootful Podman keep separate images, containers, volumes, and storage.

2. Why does my container show uid=0(root) but I am not root on the host?

Rootless Podman maps the host user into container UID 0 inside a user namespace. That root is namespace-local. It cannot gain host privileges the unprivileged account does not already have.

3. How do I fix there might not be enough IDs available in the namespace?

Confirm /etc/subuid and /etc/subgid contain sufficient ranges for the user, stop running containers, run podman system migrate to refresh the rootless pause namespace, then verify podman unshare cat /proc/self/uid_map shows both the host user mapping and the subordinate range.

4. Do I need loginctl enable-linger to run podman run interactively?

No. Linger is for user services and rootless containers that must survive logout or start at boot without an interactive login. A normal SSH or desktop session can run podman run without linger enabled.

5. Can rootless Podman work when my home directory is on NFS?

The home directory can be NFS-backed, but default container storage under HOME/.local/share/containers/storage should live on local storage that supports rootless UID mapping. Move graphroot to a local path in storage.conf rather than assuming rootless Podman cannot run at all with an NFS home.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)