Skip to content

Podman & Quadlets

Notes from setting up rootless Podman containers managed via Quadlets on my home server.


What Are Quadlets?

Quadlets let you define containers as systemd units using simple INI-style files instead of writing raw service files. Podman’s generator converts them automatically.

  • Rootless: ~/.config/containers/systemd/
  • System-level: /etc/containers/systemd/
  • Requires Podman 4.4+

Setting Up a Rootless Podman User

The idea is to run a dedicated user whose only job is running containers. No sudo, no root access - if something in a container goes sideways it stays contained.

Create the User

useradd -m -s /bin/bash podman
passwd podman

Set Up Subuid/Subgid Mappings

Rootless containers need a range of subordinate UIDs and GIDs to map container users to host users without root privileges.

# Check if they were created automatically
cat /etc/subuid
cat /etc/subgid

# If not, add them manually
usermod --add-subuids 100000-165535 --add-subgids 100000-165535 podman

Each user needs a unique, non-overlapping range. 65536 IDs is the standard allocation.

Enable Lingering

Without lingering, systemd kills the user’s services when they log out. This keeps them running.

loginctl enable-linger podman

Always Log In Properly

This trips people up. If you su podman from root, XDG_RUNTIME_DIR doesn’t get set and things break in subtle ways - sockets don’t appear where expected, %t expands to nothing in Quadlet files, systemd user services don’t behave.

Always use:

machinectl shell podman@
# or
su -l podman

Verify Rootless Works

podman run --rm hello-world

If that works, you’re good to go.


Basic Structure

[Unit]
Description=My Container
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/myimage:latest
ContainerName=my-container
Volume=%h/mydata:/data:Z

[Service]
Restart=always
RestartSec=10s

[Install]
WantedBy=default.target

Useful systemd specifiers:

  • %h - expands to $HOME
  • %t - expands to $XDG_RUNTIME_DIR (e.g. /run/user/1000)

After adding or changing a .container file:

systemctl --user daemon-reload

Networking

Quadlet-managed networks get prefixed with systemd- internally. A traefik.network file becomes systemd-traefik in podman.

Check what’s actually there:

podman network ls

Reference a Quadlet-managed network:

Network=traefik.network

Reference a manually-created network:

Network=traefik

Automatic Image Updates

Add AutoUpdate=registry to your [Container] section - no label needed:

[Container]
Image=docker.io/myimage:latest
AutoUpdate=registry

Enable the timer:

# Rootless
systemctl --user enable --now podman-auto-update.timer

# System-level
systemctl enable --now podman-auto-update.timer

Preview what would update without doing anything:

podman auto-update --dry-run

Auto-updating :latest is risky in production - a bad upstream change deploys automatically with no review.


Verifying Quadlet Units

# Check for parse errors
/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun 2>&1

# List generated units
systemctl --user list-unit-files | grep <name>

# Validate a specific file
systemd-analyze --user verify ~/.config/containers/systemd/mycontainer.container

Troubleshooting

Symptom Cause Fix
Unit not found after daemon-reload File wrong location or parse error Run podman-system-generator --user --dryrun
statfs /podman/podman.sock: no such file %t not expanding - empty XDG_RUNTIME_DIR Hardcode socket path with your UID
Socket missing after start podman.socket Socket path mismatch Check systemctl --user cat podman.socket, add override
Restart= ignored / unit fails Restart= in [Container] instead of [Service] Move it to [Service]