Managing containers as systemd services with Quadlet: declarative .container, .pod, .volume, and .image units with auto-start, restart policies, and socket activation, plus podman generate systemd to convert running containers into service units.

In episode 9 you built your own images with a Containerfile and Buildah. Now it's time to turn containers into system-managed services. Episode 10 covers Quadlet — Podman's way of declaring containers as systemd units — along with auto-start, restart policies, socket activation, and podman generate systemd for production workloads.
Running containers via manual podman run isn't suitable for production: if the host reboots, the container won't come back up; if the process crashes, nothing recovers it. Podman ships with native systemd integration, and Quadlet simplifies it: you write a simple unit file, and systemd keeps the container alive like a regular service.
Quadlet works by reading declarative files and converting them into full systemd units — podman run is executed behind the scenes according to that description. Files are placed in a directory scanned by systemd, and every change requires a daemon reload.
| Unit | Function |
|---|---|
.container | Describes a single container |
.pod | Describes a single pod |
.volume | Describes a named volume |
.image | Describes an automatically pulled image |
Each unit file produces a related systemd unit: .container becomes container-<name>.service, .pod becomes pod-<name>.service, and so on. .volume and .image units typically become dependencies managed at start — for example .image ensures the image is pulled before the container runs.
The .container file uses the familiar INI format:
[Unit]
Description=Web App container
After=network-online.target
[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
Volume=webapp-data:/usr/share/nginx/html:Z
[Install]
WantedBy=default.targetThe [Unit] section manages dependencies and description, [Container] describes the container (image, port, volume), and [Install] defines when the unit is enabled. The values mirror podman run flags: Image= is equivalent to --image, PublishPort= to -p, Volume= to -v.
After the file is written, systemd must recognize it:
systemctl --user daemon-reload
systemctl --user start webapp
systemctl --user enable webappdaemon-reload makes systemd read the new unit, start runs the container, and enable makes it start automatically when the user logs in. With --user, the unit runs in a rootless user session — consistent with the Podman style you've learned since episode 5.
Quadlet's main advantage is that the container is started and recovered by systemd. Two mechanisms work together:
enabled start automatically at boot or when the user session begins.RestartPolicy value | Behavior |
|---|---|
always | Always restart, whatever the exit code |
on-failure | Restart only when the container fails |
unless-stopped | Restart unless explicitly stopped |
With the combination of auto-start and RestartPolicy=always, a container becomes a genuinely system-managed service — a crash no longer means downtime without action.
Important
systemctl --user daemon-reload is mandatory every time a Quadlet file changes, and after editing RestartPolicy or container configuration, use systemctl --user restart webapp — a new container is only started according to the latest definition after a reload.
Not just containers — the entire workload layer can be declared. Volumes and pods are used as dependencies by .container units:
[Volume]
Driver=local[Pod]
Network=backend-net
PublishPort=9000:9000webapp-data.volume describes a named volume, and backend.pod describes a pod. A .container unit can link itself to that volume or pod. Because systemd manages dependencies between units, the start order — volume mounted first, pod started, then container joining — is handled automatically.
A container doesn't always need to be running. With socket activation, systemd listens on the port first, and only when a connection arrives is the container started — an on-demand service:
[Container]
Image=quay.io/example/worker:latest
[Install]
WantedBy=default.target[Socket]
ListenStream=127.0.0.1:9100
[Install]
WantedBy=sockets.targetThe .socket unit makes systemd listen on the port; the first connection triggers the start of the related container unit. Rarely used services — admin tools, internal endpoints — can sit idle without consuming resources, waking up only when truly needed.
For already-running containers, systemd units can be generated automatically:
podman generate systemd --new --name webapp
podman generate systemd --new --files --name webapppodman generate systemd --new --name webapp prints a systemd unit for the webapp container to stdout; the --files option writes it to a file. The --new flag makes the unit start a new container (with the same definition) on every start, rather than attaching to the old container — the behavior best suited for a service.
Putting it all together, a production pattern with Quadlet looks like this:
.volume and .image units for dependencies..container unit with Image, PublishPort, Volume, and RestartPolicy.systemctl --user daemon-reload, start, and enable.systemctl --user status, journalctl --user -u webapp.systemctl --user status webapp
journalctl --user -u webapp -fContainer logs now flow into the journal — journalctl becomes the single observation window for all services, exactly like managing non-container services.
Episode 10 covered managing containers as systemd services: the Quadlet concept and its advantages, the .container, .pod, .volume, and .image unit types, writing declarative units with auto-start and restart policies, socket activation for on-demand services, podman generate systemd for conversion, and the production service management pattern.
The key points to take home:
always for services that must always stay alive.systemctl --user daemon-reload after every unit change is a mandatory habit.In the next episode, Episode 11, you'll dive into configuration and storage: containers.conf, registries.conf, storage.conf, policy.json, registries.d, and certs.d, the big configuration rework in Podman 6 with unified lookup and XDG_CONFIG_HOME, up to the rootless vs rootful overlayfs storage layout.