$linuxjunkies
>

Rootless Podman for Server Workloads

Run production containers without root using Podman's user namespaces, systemd Quadlets, subuid/subgid mapping, and rootless networking on Linux servers.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • Podman 4.6 or later installed (Podman 5+ recommended for pasta networking)
  • A Linux kernel 5.11+ for native overlayfs in user namespaces
  • sudo or root access to create the service account and set subuid/subgid ranges
  • systemd running as PID 1 with pam_systemd active for linger support

Rootless Podman lets you run containers without a privileged daemon and without root. For server workloads that means a smaller attack surface: a compromised container cannot easily escape to a root-owned process. The tradeoff is that Linux user namespaces, subordinate UID/GID ranges, and a few networking quirks require deliberate configuration. This guide walks through every piece—from subuid mapping to systemd Quadlets managing your containers as proper services.

Understanding Subordinate UID/GID Mappings

Rootless containers run inside a user namespace. The Linux kernel maps a range of host UIDs (owned by your unprivileged user) into the full 0–65535 range inside the container. Two files control this: /etc/subuid and /etc/subgid.

A typical entry looks like:

grep appuser /etc/subuid /etc/subgid

Which outputs something like:

/etc/subuid:appuser:100000:65536
/etc/subgid:appuser:100000:65536

This means host UIDs 100000–165535 map to UIDs 0–65535 inside containers run by appuser. Most distributions assign these ranges automatically when you create a user, but dedicated service accounts often need manual entries.

Creating a Dedicated Service Account

Always run server workloads under a locked, dedicated account—not your personal login.

sudo useradd -r -m -s /sbin/nologin -d /home/appuser appuser

If subuid/subgid entries were not created automatically, add them:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 appuser

Verify:

grep appuser /etc/subuid /etc/subgid

After changing these files, regenerate the user namespace configuration:

sudo -u appuser podman system migrate

Enabling Lingering for the Service Account

By default, systemd destroys user sessions when the user logs out. For a headless service account that never logs in interactively, enable lingering so its user services start at boot and keep running.

sudo loginctl enable-linger appuser

Confirm it took effect:

loginctl show-user appuser | grep Linger

Expected output: Linger=yes

Rootless Networking

Rootless Podman cannot create traditional bridge interfaces because that requires CAP_NET_ADMIN. Two practical approaches cover most server use cases.

Pasta (Default on Podman 5+)

Podman 5 ships pasta as the default network backend for rootless containers. Pasta uses a user-space network stack that mirrors the host's network interfaces without any privileged helper. No additional configuration is needed on most systems.

sudo -u appuser podman info --format '{{.Host.NetworkBackend}}'

You should see netavark listed for the engine, with pasta handling the rootless network namespace. Port mapping works transparently:

sudo -u appuser podman run -p 8080:80 docker.io/library/nginx:stable-alpine

If your distro ships Podman 4.x, install passt and set the network backend, or use slirp4netns which ships with older Podman builds.

Exposing Ports Below 1024

Unprivileged users cannot bind ports below 1024 by default. For production, the cleanest solutions are:

  • Bind on a high port (e.g., 8443) and use a firewall redirect or a reverse proxy (nginx, Caddy).
  • Lower the net.ipv4.ip_unprivileged_port_start sysctl—set it permanently in /etc/sysctl.d/:
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-rootless-ports.conf
sudo sysctl --system

Setting this to 80 lets any unprivileged user bind port 80 and above. Evaluate your threat model before applying it.

Mounting Volumes

Volume mounts in rootless containers interact with the user namespace. The host path is accessed as the host user (appuser), but inside the container the process may run as root (UID 0), which maps to appuser's UID on the host.

Creating and Labeling Volume Directories

sudo mkdir -p /srv/appdata
sudo chown -R appuser:appuser /srv/appdata

On SELinux-enforcing systems (Fedora, RHEL, Rocky), label the directory so Podman can access it:

sudo chcon -Rt container_file_t /srv/appdata

Alternatively, append :Z to the mount in your container definition to relabel automatically—but use :z (shared) when multiple containers need the same directory.

Named Podman Volumes

Named volumes live under the service account's home directory and avoid SELinux label issues entirely. Prefer them when the data doesn't need to be accessed from the host directly.

sudo -u appuser podman volume create appdata
sudo -u appuser podman volume inspect appdata --format '{{.Mountpoint}}'

Systemd Quadlets

Quadlets (introduced in Podman 4.4, stable in 4.6+) let you define containers as .container unit files that systemd-generator converts into real service units. This is the modern replacement for podman generate systemd, which is deprecated.

Directory Layout

User-level Quadlets go in the service account's systemd config directory. Because appuser has no interactive login, create the path and drop in the unit as root, then fix ownership:

sudo mkdir -p /home/appuser/.config/containers/systemd
sudo chown -R appuser:appuser /home/appuser/.config

Writing a .container Quadlet File

Create /home/appuser/.config/containers/systemd/myapp.container:

sudo -u appuser tee /home/appuser/.config/containers/systemd/myapp.container <<'EOF'
[Unit]
Description=My Application Container
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/nginx:stable-alpine
PublishPort=8080:80
Volume=/srv/appdata:/usr/share/nginx/html:ro,Z
Environment=NGINX_HOST=example.com
Label=app=myapp
AutoUpdate=registry

[Service]
Restart=always
RestartSec=5
TimeoutStartSec=120

[Install]
WantedBy=default.target
EOF

Enabling and Starting the Service

Reload the systemd user daemon as appuser to trigger the generator:

sudo -u appuser XDG_RUNTIME_DIR=/run/user/$(id -u appuser) \
  systemctl --user daemon-reload
sudo -u appuser XDG_RUNTIME_DIR=/run/user/$(id -u appuser) \
  systemctl --user enable --now myapp.service

Systemd derives the unit name from the file name; myapp.container becomes myapp.service.

Quadlet for a Pod (Multi-Container)

For services with a sidecar or a separate database, create a .pod file and reference it from multiple .container files:

sudo -u appuser tee /home/appuser/.config/containers/systemd/webapp.pod <<'EOF'
[Pod]
PublishPort=8080:80
EOF
sudo -u appuser tee /home/appuser/.config/containers/systemd/webapp-app.container <<'EOF'
[Container]
Image=docker.io/myorg/webapp:latest
Pod=webapp.pod
Volume=appdata:/data:Z
EOF

All containers in the pod share the same network namespace, so they communicate via localhost.

Verification

Check the service is running and the container is healthy:

sudo -u appuser XDG_RUNTIME_DIR=/run/user/$(id -u appuser) \
  systemctl --user status myapp.service
sudo -u appuser podman ps --filter label=app=myapp

Inspect journal logs:

sudo journalctl _UID=$(id -u appuser) -u myapp.service -f

Test the published port from the host:

curl -I http://localhost:8080

Auto-Update with Podman

The AutoUpdate=registry label in the Quadlet enables podman auto-update, which checks for newer image digests and restarts the service. Wire it to a systemd timer for fully automated updates:

sudo -u appuser XDG_RUNTIME_DIR=/run/user/$(id -u appuser) \
  systemctl --user enable --now podman-auto-update.timer

Troubleshooting

  • "newuidmap" errors at container start: The newuidmap/newgidmap binaries must be setuid root. Install uidmap (Debian/Ubuntu) or shadow-utils (Fedora/RHEL) and verify with ls -l /usr/bin/newuidmap.
  • Permission denied on bind mount: The container process's effective UID inside the namespace may not map to an owner the host recognizes. Use podman unshare chown -R 0:0 /srv/appdata to set ownership relative to the container's root inside the user namespace.
  • Service not starting at boot (linger issue): Confirm loginctl show-user appuser | grep Linger returns yes. If XDG_RUNTIME_DIR is missing at boot, ensure systemd creates it: check that pam_systemd is active and User=appuser is not missing from service units.
  • SELinux AVC denials: Run ausearch -m avc -ts recent and use audit2why to decode. The :Z mount option resolves most container file access denials.
  • Quadlet unit not appearing after daemon-reload: Run sudo -u appuser /usr/lib/systemd/system-generators/podman-system-generator --user --dry-run to see generator output and catch syntax errors in your .container file.
tested on:Fedora 40Ubuntu 24.04Rocky 9Arch rolling

Frequently asked questions

Can rootless Podman containers talk to each other on the same host?
Yes. Create a Podman user network ('podman network create mynet') and attach containers to it via 'Network=mynet' in their Quadlet files. Within a pod, containers share localhost. Across pods or standalone containers, use the network name and container hostname.
Is rootless Podman significantly slower than rootful Podman?
User namespace overhead is measurable but minor for most workloads—typically under 2% CPU difference. The bigger consideration is storage: the fuse-overlayfs driver (used when kernel overlay in userns is unavailable) is slower than native overlayfs. Kernel 5.11+ supports native overlay in user namespaces, which modern distros use by default.
How do I update the container image in a Quadlet-managed service?
Pull the new image manually ('podman pull <image>') then restart the service ('systemctl --user restart myapp.service'). With AutoUpdate=registry set, 'podman auto-update' (or its timer) handles this automatically by comparing the registry digest to the local one.
Why does 'podman generate systemd' no longer appear in the docs?
Podman deprecated 'generate systemd' in favour of Quadlets starting in version 4.4. Quadlets are maintained by systemd's generator infrastructure, support pods natively, and allow declarative unit files rather than generated boilerplate. Use .container files going forward.
What happens to the containers if the host reboots before linger is enabled?
Without lingering, the user's systemd instance never starts at boot, so no user services run—including container services. The containers will not start until someone logs in as that user. Enabling linger with 'loginctl enable-linger' is mandatory for server use.

Related guides