Skip to content

Getting Started with Quadlet

Applies to AlmaLinux 9.4+ & 10 / Rocky Linux 9.4+ & 10 / CentOS Stream 9 & 10 (Podman 4.4+)

To make a container start automatically at boot and restart after a crash, you used to run podman generate systemd to produce a service unit. That approach is now deprecated. Since Podman 4.4, Podman ships with Quadlet: you write a single declarative .container file, and systemd converts it into a service unit at startup. The configuration is cleaner and much easier to change.

  • What Quadlet is and why it replaces podman generate systemd
  • Where unit files go (root vs. rootless)
  • How to write an nginx.container file and start it with systemd
  • How to configure rootless autostart and automatic image updates
  • What the other unit types (.volume, .network, .pod, etc.) are for
  • A system running EL 9.4+ or EL 10
  • Podman 4.4 or newer (included by default on EL 9.4+/EL 10)
  • A regular user with sudo privileges

Quadlet is a systemd generator built into Podman since 4.4. You hand systemd a .container file describing a container, and on daemon-reload systemd invokes Quadlet to generate the matching .service unit automatically.

Its advantage is being declarative: you describe “what you want” rather than producing a “snapshot of the current state” as podman generate systemd does. When container parameters change, the old approach requires regenerating the unit file, whereas with Quadlet you just edit the file and daemon-reload.

Quadlet scans fixed directories. Placing the file correctly is a prerequisite for it to work:

ScenarioDirectory
System level (root)/etc/containers/systemd/ (or the distro-provided /usr/share/containers/systemd/)
User level (rootless)~/.config/containers/systemd/

The following walks through the full flow with an Nginx container. First create the unit file:

nginx.container
[Unit]
Description=Nginx web server
[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
Volume=/srv/nginx/html:/usr/share/nginx/html:Z
Environment=TZ=Asia/Shanghai
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=default.target

A few key points:

  • Image= should use the full image path (docker.io/library/...) to avoid a short-name triggering an interactive registry prompt.
  • PublishPort= is equivalent to podman run -p; here it maps host 8080 to container 80.
  • The :Z suffix on Volume= applies the correct SELinux label automatically (a private label) on SELinux systems, equivalent to podman run -v ...:Z.
  • AutoUpdate=registry enables automatic image updates (see below).
  • The service name is the filename without its extension: nginx.container → nginx.service.

Then start it depending on whether you run as root or rootless.

Place the file in /etc/containers/systemd/, then:

  1. Reload so systemd generates the matching nginx.service:

    Reload units
    $ sudo systemctl daemon-reload
  2. Start the service (note the name is nginx, not nginx.container):

    Start and check status
    $ sudo systemctl start nginx
    $ sudo systemctl status nginx

After adding AutoUpdate=registry to [Container], enable the corresponding timer and Podman will periodically check whether the upstream image has changed and roll it forward automatically:

Enable the auto-update timer
$ sudo systemctl enable --now podman-auto-update.timer
Trigger once manually (optional)
$ podman auto-update

Quadlet is not limited to .container. The common unit types are:

File suffixPurpose
.containerA single container
.volumeA named volume, referenced by a .container’s Volume=
.networkA custom network, referenced by Network=
.podA group of containers sharing a network namespace (a Pod)
.kubeRun Kubernetes YAML directly (play kube)
.imagePre-pull an image for other units to depend on

For example, declare a volume first, then reference it from a container:

app-data.volume
[Volume]
# Creates a volume named systemd-app-data
app.container (excerpt)
[Container]
Image=docker.io/library/postgres:16
Volume=app-data.volume:/var/lib/postgresql/data:Z

If you still use the container-*.service files generated by podman generate systemd, migration is straightforward: translate each podman run argument into a field in the [Container] section.

podman run argumentQuadlet field
--name (optional)ContainerName=
-p 8080:80PublishPort=8080:80
-v src:dst:ZVolume=src:dst:Z
-e KEY=valEnvironment=KEY=val
--restart=alwaysRestart=always in the [Service] section

After migrating, disable and remove the old container-*.service to avoid port/name conflicts.

Edited the .container file but nothing changed? Quadlet only regenerates units on daemon-reload. After every edit, run systemctl daemon-reload (add --user for rootless), then restart the service.

systemctl start nginx.container says the unit isn’t found? The service name is the filename without its extension: nginx.container maps to nginx.service, so run systemctl start nginx, not the .container form.

Rootless container stops as soon as you log out? Linger isn’t enabled. Run sudo loginctl enable-linger $(whoami) so user services keep running while you’re offline.

Startup hangs asking you to choose a registry? Image= used a short-name (like nginx), triggering the interactive prompt. Write the full image path docker.io/library/nginx:latest in the unit file.

Container can’t read mounted files (permission denied)? SELinux is blocking it. Add the :Z suffix to Volume=, e.g. Volume=/srv/data:/data:Z, so Podman applies the correct SELinux label.