Before you start
- Podman 4.4 or later:
podman --version - An unprivileged user account dedicated to services (here
spo) - Being comfortable with systemd (
systemctl,journalctl)
In plain words
A container is an application delivered in its box with everything it needs. Quadlet is the job sheet you hand to the building's caretaker (systemd): "open this box every morning, restart it if it stops, write everything down in the logbook". And the caretaker does it for an ordinary tenant, without needing the owner's keys (root).
Docker Compose starts containers, but it needs a daemon and root rights, and its integration with the system stays external. Quadlet, built into Podman since version 4.4, turns a .container file into a real systemd service: start at boot, restart on failure, logs in journalctl, dependencies, all without root.
The principle
You write a declarative file. On daemon-reload, a systemd generator turns it into a regular .service unit.
| Location | For |
|---|---|
~/.config/containers/systemd/ |
A user's rootless containers |
/etc/containers/systemd/ |
Containers run as root (avoid if possible) |
The file web.container becomes the service web.service.
1. Let the user's services run without a session
By default, user services stop when the user logs out and do not start at boot. Enable lingering:
sudo loginctl enable-linger spo
loginctl show-user spo --property=Linger # expected: Linger=yes
2. Write the Quadlet file
Example: an nginx server publishing a static folder.
mkdir -p ~/.config/containers/systemd ~/site
echo '<h1>It works</h1>' > ~/site/index.html
nano ~/.config/containers/systemd/web.container
[Unit]
Description=Static web server
[Container]
ContainerName=web
Image=docker.io/library/nginx:stable
PublishPort=8080:80
# %h = home folder; ro = read-only; Z = private SELinux label
Volume=%h/site:/usr/share/nginx/html:ro,Z
# Automatic update from the registry (see step 5)
AutoUpdate=registry
# Security: no privilege escalation possible inside the container
NoNewPrivileges=true
[Service]
Restart=on-failure
TimeoutStartSec=300
[Install]
WantedBy=default.target
Privileged ports
An unprivileged user cannot listen on a port below 1024. Publish on a high port (8080) and put a reverse proxy in front, or lower net.ipv4.ip_unprivileged_port_start knowing what it implies.
3. Generate and start the service
systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service
curl -s http://localhost:8080
Important
Do not use systemctl --user enable web.service: the unit is generated, so the command fails. Starting at boot is handled by the [Install] section of the .container file.
What about waiting for the network?
No need to add After=network-online.target: a user service cannot wait for a system target, so the line would be ignored. In rootless mode, Quadlet adds a dependency on podman-user-wait-network-online.service by itself.
If something is wrong, the generator simply does not create the service. To find out why:
# Generation errors and checks on the generated service
systemd-analyze --user --generators=true verify web.service
# Show the generated units
/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun
Since Podman 5.6, podman quadlet list shows the installed Quadlet files and their status.
4. Logs and day-to-day operation
journalctl --user -u web.service -f # live logs
systemctl --user restart web.service # restart
podman ps # the usual Podman view
After every change to the .container file: systemctl --user daemon-reload, then restart.
5. Automatic updates
With AutoUpdate=registry, Podman compares the local image with the registry's and restarts the service if a new version exists. The image must be referenced by its fully qualified name, registry included (docker.io/library/nginx, not just nginx). A timer shipped with Podman takes care of it every night at midnight:
systemctl --user enable --now podman-auto-update.timer
podman auto-update --dry-run # see what would be updated
If the new container fails to start, Podman automatically rolls back to the previous image (rollback, enabled by default).
Warning
Keep auto-update for images whose tag follows compatible versions (stable, 1.2, etc.). On a latest tag that can jump to a new major version, you risk an unwanted migration. For images you build locally, use AutoUpdate=local.
6. Several containers talking to each other
Quadlet also handles networks, volumes and pods with .network, .volume and .pod files. Example of a shared network:
# ~/.config/containers/systemd/app.network
[Network]
NetworkName=app
Then, in each .container:
[Container]
Network=app.network
Containers on the same network reach each other by name (ContainerName), and systemd starts the network before them.
Key takeaways
| Action | Command |
|---|---|
| Pick up a modified file | systemctl --user daemon-reload |
| Start, stop, restart | systemctl --user start name.service (or stop, restart) |
| Understand a generation error | systemd-analyze --user --generators=true verify name.service |
| Logs | journalctl --user -u name.service |
| Start at boot without a session | sudo loginctl enable-linger <user> |