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 You Will Learn
Section titled “What You Will Learn”- What Quadlet is and why it replaces
podman generate systemd - Where unit files go (root vs. rootless)
- How to write an
nginx.containerfile 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
Prerequisites
Section titled “Prerequisites”- 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
What Is Quadlet
Section titled “What Is Quadlet”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.
Where Unit Files Go
Section titled “Where Unit Files Go”Quadlet scans fixed directories. Placing the file correctly is a prerequisite for it to work:
| Scenario | Directory |
|---|---|
| System level (root) | /etc/containers/systemd/ (or the distro-provided /usr/share/containers/systemd/) |
| User level (rootless) | ~/.config/containers/systemd/ |
Your First .container File
Section titled “Your First .container File”The following walks through the full flow with an Nginx container. First create the unit file:
[Unit]Description=Nginx web server
[Container]Image=docker.io/library/nginx:latestPublishPort=8080:80Volume=/srv/nginx/html:/usr/share/nginx/html:ZEnvironment=TZ=Asia/ShanghaiAutoUpdate=registry
[Service]Restart=always
[Install]WantedBy=default.targetA 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 topodman run -p; here it maps host 8080 to container 80.- The
:Zsuffix onVolume=applies the correct SELinux label automatically (a private label) on SELinux systems, equivalent topodman run -v ...:Z. AutoUpdate=registryenables 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:
-
Reload so systemd generates the matching
nginx.service:Reload units $ sudo systemctl daemon-reload -
Start the service (note the name is
nginx, notnginx.container):Start and check status $ sudo systemctl start nginx$ sudo systemctl status nginx
Place the file in ~/.config/containers/systemd/, then use --user:
-
Reload the user instance:
Reload user units $ systemctl --user daemon-reload -
Start the service:
Start and check status $ systemctl --user start nginx$ systemctl --user status nginx -
Keep user services running after you log out:
Enable linger $ sudo loginctl enable-linger $(whoami)
Automatic Image Updates
Section titled “Automatic Image Updates”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:
$ sudo systemctl enable --now podman-auto-update.timer$ systemctl --user enable --now podman-auto-update.timer$ podman auto-updateOther Unit Types
Section titled “Other Unit Types”Quadlet is not limited to .container. The common unit types are:
| File suffix | Purpose |
|---|---|
.container | A single container |
.volume | A named volume, referenced by a .container’s Volume= |
.network | A custom network, referenced by Network= |
.pod | A group of containers sharing a network namespace (a Pod) |
.kube | Run Kubernetes YAML directly (play kube) |
.image | Pre-pull an image for other units to depend on |
For example, declare a volume first, then reference it from a container:
[Volume]# Creates a volume named systemd-app-data[Container]Image=docker.io/library/postgres:16Volume=app-data.volume:/var/lib/postgresql/data:ZMigrating From the Old Approach
Section titled “Migrating From the Old Approach”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 argument | Quadlet field |
|---|---|
--name (optional) | ContainerName= |
-p 8080:80 | PublishPort=8080:80 |
-v src:dst:Z | Volume=src:dst:Z |
-e KEY=val | Environment=KEY=val |
--restart=always | Restart=always in the [Service] section |
After migrating, disable and remove the old container-*.service to avoid port/name conflicts.
Common Issues
Section titled “Common Issues”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.