Rootless Podman with Quadlet: the one tip that matters
I run a handful of small services as containers on a Linux host: a status page, a few internal tools, the odd experiment. For a while I used Docker Compose for everything, but I’ve gradually moved the simple ones to rootless Podman managed by Quadlet. The result is containers that run as an ordinary user, start at boot, restart on failure and log to the journal, all without a daemon running as root.
Here’s the short version of how to set it up, plus the one tip that saves the most confusion.
What Quadlet is
Quadlet ships with Podman 4.4 and later. You write a small unit-style file describing a container, and a systemd generator turns it into a proper service at boot or on daemon-reload. Instead of hand-writing podman run commands into service files, or using the older podman generate systemd, you describe what you want and systemd handles the lifecycle.
A minimal rootless container
For a rootless service, unit files go in your user’s config directory:
mkdir -p ~/.config/containers/systemd
Create ~/.config/containers/systemd/whoami.container:
[Unit]
Description=Simple whoami test service
[Container]
Image=docker.io/traefik/whoami:latest
PublishPort=8080:80
AutoUpdate=registry
[Service]
Restart=on-failure
[Install]
WantedBy=default.target
Then reload and start it:
systemctl --user daemon-reload
systemctl --user start whoami.service
systemctl --user status whoami.service
Notice there’s no enable step. Quadlet units are generated, and the [Install] section takes care of starting them at boot. Running systemctl --user enable on a generated unit will just give you an error.
The tip: enable lingering
This is the one that bites almost everyone. By default, a user’s systemd instance only runs while that user is logged in. Your rootless container starts fine while you’re in an SSH session, then disappears the moment you log out, and doesn’t come back after a reboot until you log in again.
The fix is to enable lingering for the service account:
sudo loginctl enable-linger <username>
With lingering on, the user manager starts at boot and keeps running regardless of logins, so your default.target services come up with the machine. Check it with loginctl show-user <username> --property=Linger.
A few more things worth knowing
Use a dedicated user. I create a non-login service account per group of related containers. If a container is compromised, the attacker lands as an unprivileged user with access to very little.
Low ports need a tweak. Rootless containers can’t bind to ports below 1024 by default. Either publish on a high port and put a reverse proxy in front, or lower net.ipv4.ip_unprivileged_port_start if you understand the trade-off.
Volumes and SELinux. On SELinux-enabled systems, add :Z to bind mounts, for example Volume=%h/whoami-data:/data:Z, so the container is allowed to read them. %h expands to the user’s home directory.
Debug the generator. If a unit doesn’t appear after a reload, run the generator directly to see parsing errors:
/usr/libexec/podman/quadlet -dryrun -user
The path varies slightly by distribution, but the output tells you exactly which line it didn’t like.
Automatic updates. With AutoUpdate=registry set, enable the podman-auto-update.timer for your user and Podman will pull newer images and restart the service, rolling back if the new container fails to start. I only do this for low-risk services and pin versions for anything important.
When I still reach for Compose
Quadlet supports networks, volumes, pods and Kubernetes YAML, so it can handle multi-container apps. For a quick experiment with five interdependent containers, though, Compose is often faster to iterate on. My rule of thumb: experiments start in Compose, and anything that becomes a long-lived service graduates to Quadlet.
Why bother?
Rootless Podman with Quadlet gives you containers that behave like any other systemd service. They have the same systemctl commands, the same journalctl logs and the same dependency handling, with a much smaller blast radius than a root daemon. Once lingering is enabled, it simply works.
Questions or corrections? Email me.
