Running Containers as systemd Services with Podman Quadlet
Podman Quadlet lets you run containers as systemd services using declarative .container files — rootless, daemonless, and integrated with systemd's dependency and logging model. Covers a working example, the generator mechanism, and the pitfalls that break production deployments.
07 Aug 2025, 23:30 UTC

The Problem: Managing Containers Without a Root Daemon
You want containers to start at boot, restart on failure, and integrate with systemd logging and dependencies — but you don't want a root-owned daemon running on your server. Podman's Quadlet feature solves this by letting you describe containers, volumes, networks, and pods as declarative .container files. Systemd's generator then creates proper user units from them at startup, so containers run rootless, without a persistent daemon, and behave like any other systemd service.
Minimal Working Example
Create ~/.config/containers/systemd/webapp.container with the following content. This runs an nginx container publishing port 8080, mounts a config directory, and enables auto-start via user lingering.
[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80
Volume=%h/webapp/config:/etc/nginx/conf.d:Z
[Install]
WantedBy=default.target
Run these commands as the deploying user (not root):
systemctl --user daemon-reload
systemctl --user start webapp.service
systemctl --user enable --now webapp.service
loginctl enable-linger $(whoami)
Where to run: Any Linux host with Podman 4.4+ and systemd. Permissions: Regular user account; no sudo required. Check: systemctl --user status webapp.service should show active (running). Risk: Port 8080 must be free; SELinux will deny the volume mount without the :Z suffix.
How the Generator Works
When you run systemctl --user daemon-reload, systemd invokes /usr/lib/systemd/user-generators/podman-user-generator. This generator reads every .container, .volume, .network, and .pod file under ~/.config/containers/systemd/ (and the system-wide /etc/containers/systemd/) and emits native .service units. The generated unit runs podman run with the exact flags you declared, handles PID 1 signal forwarding correctly, and registers the unit under systemd's supervision.
Because the generator runs at each daemon-reload, edits to the .container file take effect after the next reload — no manual unit editing required.
Key Directives You'll Use
| Directive | Section | Purpose |
|---|---|---|
Image= | [Container] | Container image reference (registry/name:tag) |
PublishPort= | [Container] | Host:container port mapping; repeat for multiple ports |
Volume= | [Container] | Bind mount or named volume; add :Z for SELinux relabeling |
Environment= | [Container] | Environment variables; supports EnvironmentFile= for secrets |
HealthCmd= | [Container] | Command run inside container for health checks |
AutoUpdate=registry | [Container] | Enables podman auto-update to pull newer images |
After=, Requires= | [Unit] | Systemd dependencies on other generated units |
WantedBy=default.target | [Install] | Starts the unit when the user session reaches default.target |
Rootless Boot Persistence: The Linger Requirement
User systemd units tied to default.target only run while the user has an active session. For servers, enable user lingering so the user's systemd instance starts at boot and stays running after logout:
loginctl enable-linger $(whoami)
Run once per deploying user. Verify with loginctl show-user $(whoami) -p Linger — it should return Linger=yes. Without this, containers stop when you close your SSH session.
Common Pitfalls and Limits
Podman Version Compatibility
Quadlet was introduced in Podman 4.0 and stabilized in 4.4. Older releases (pre-4.x) lack the generator entirely. Check your version:
podman --version
ls /usr/lib/systemd/user-generators/podman-user-generator
If the generator binary is missing, your distribution's Podman package is too old.
Privileged Ports Below 1024
Rootless containers cannot bind ports below 1024 by default. Options:
- Use a reverse proxy (nginx, Traefik) on port 80/443 forwarding to high ports.
- Raise the unprivileged port range:
sysctl net.ipv4.ip_unprivileged_port_start=80(persist in/etc/sysctl.d/). Requires root.
SELinux Volume Labels
On Fedora, RHEL, and derivatives, bind mounts need :Z (private relabel) or :z (shared relabel) suffixes in the Volume= path. Omitting this causes Permission denied errors inside the container. Example: Volume=/opt/data:/data:Z.
No Daemon Means No Central Socket
Unlike Docker, there's no podman.sock to expose to CI/CD tools. For remote API access, run the rootless podman system service as a separate Quadlet unit or use SSH tunneling.
Verification Checklist
podman --versionis 4.4 or newer and the generator binary exists.- Create a test
.containerfile, runsystemctl --user daemon-reload. systemctl --user status webapp.serviceshowsactive (running).podman inspect webappconfirms published ports, volumes, and SELinux labels match the Quadlet file.- Reboot or log out and back in, then check
systemctl --user status webapp.service— it should still be running, confirming linger works. - Run
podman auto-update --dry-runto verifyAutoUpdate=registryworks.
When to Choose Quadlet Over Alternatives
- vs. hand-written systemd units calling
podman run: Quadlet removes boilerplate, handles container cleanup on stop, and is upstream's recommended path. - vs. Docker Compose plus systemd: No daemon, rootless by default, native systemd dependency handling.
- vs. Kubernetes: Quadlet is for single-host deployments; use Podman's
.kubegenerator or k3s for multi-host.
Limitations to Know
- Quadlet syntax and supported keys change between Podman minor versions; always test after upgrades.
- No built-in secret rotation —
EnvironmentFile=points to a static file you must update manually. - Network and volume units (
.network,.volume) are separate files; you cannot define them inline in.container. - Rootless cgroups v1 hosts may hit resource limit enforcement gaps; cgroups v2 is recommended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.