Podman Quadlet: A Simpler Way to Run Containers with systemd
1. Introduction: Podman’s Way of Managing Services
What is Quadlet?
Podman Quadlet is a feature that lets you manage containers using simple text files. Think of Quadlet as a translator. You create a simple file ending in .container or .volume that describes what you want to run.
When your system starts up or when you run systemctl daemon-reload, a part of systemd called a “generator” runs. Podman’s generator (podman-system-generator) finds your simple files and automatically translates them into the complex .service files that systemd knows how to read.
This is powerful because you don’t have to write the complicated service files yourself. You just describe the container, and Quadlet handles the rest.
What Problem Does Quadlet Solve?
Before Quadlet, the main way to run a container as a service was to use the podman generate systemd command. This old method had a big problem:
- You had to first manually run a container.
- Then, you ran the command to create a
.servicefile based on that running container. - You had to copy this file into a systemd folder.
- The service file this command made was “brittle,” meaning it broke easily.
When you updated Podman (e.g., from version 4 to 5), the commands or best practices might change. Your old, static .service file wouldn’t know about these changes, causing your service to fail after an update. You had to manually rebuild all your service files to fix them.
Quadlet solves this problem automatically. Because your simple .container file is “translated” every time you run systemctl daemon-reload, it’s like the service file is rebuilt every time. When you update Podman, the translator (the generator) is also updated. On the next reload, it automatically creates new, correct service files using all the latest features, all without you doing anything.
Telling Podman “What” vs. “How”
Quadlet changes how you manage containers. Instead of telling Podman how to run a container (with a long, complex command), you just tell it what you want. This is a “declarative” model, similar to Kubernetes or Docker Compose files.
You write a simple file:
Image=docker.io/nginxinc/nginx-unprivileged
PublishPort=8080:80
Quadlet translates this into the complex systemd service file for you, which might include long commands like:
ExecStart=/usr/bin/podman run --name=systemd-%N --cidfile=%t/%N.cid --replace --rm --log-driver passthrough --cgroups=split --sdnotify=conmon -d...
This makes your container definitions much easier to write, read, and manage.
Quadlet teaches your system’s main service manager (systemd) how to understand and manage containers as if they were regular system services. You can use the same commands (systemctl, journalctl) and dependency rules (After=, Wants=) for your containers as you do for everything else on the system.
2. How It Works
The Generator Mechanism
The Quadlet process is started by systemd. When you run systemctl daemon-reload (as root or with --user), systemd runs all generator programs it can find, including podman-system-generator.
This generator does the following:
- Scan: It looks in specific folders (see section 2.2) for files ending in
.container,.volume,.network,.pod,.kube,.build,.image, or.artifact. - Parse: It reads your simple file (e.g.,
myapp.container), noting both the container settings (like[Container]) and any systemd settings (like[Unit]or[Install]). - Generate: It translates your file into a full systemd
.servicefile (e.g.,myapp.service) and saves it in a temporary generator directory (like/run/systemd/generator/or~/.config/systemd/user/generator/). - Load: systemd then reads the new
myapp.servicefrom that temporary directory and loads it, just like any other service file.
Where to Put Your Quadlet Files (Root vs. Rootless)
The folder you put your Quadlet file in is very important. It decides if the container runs as the system’s “root” user (rootful) or as your normal user (rootless). The system checks these paths in order; a file in a higher-precedence path will be used over a file with the same name in a lower one.
Table 1: Quadlet Unit File Search Paths
| Scope | Path | Precedence | Purpose |
|---|---|---|---|
| Rootful | /run/containers/systemd/ |
1 | Temporary files, good for testing |
/etc/containers/systemd/ |
2 | Main location for system-wide services | |
/usr/share/containers/systemd/ |
3 | Default services included with software | |
| Rootless User | $XDG_RUNTIME_DIR/containers/systemd/ |
1 | User’s temporary files, good for testing |
$XDG_CONFIG_HOME/containers/systemd/ (or ~/.config/containers/systemd/) |
2 | Main location for your own user services | |
/etc/containers/systemd/users/$(UID) |
3 | Admin-defined services for a specific user | |
/etc/containers/systemd/users/ |
4 | Admin-defined services for any user |
A Must-Do for Rootless Users: Enable Linger
When you run containers as a regular user (rootless), there is one critical setup step you must do to run them 24/7. By default, when you log out (e.g., close your SSH session), systemd stops all of your user’s services.
To make your services “linger” or stay running after you log out, you must run this command one time:
sudo loginctl enable-linger <username>
This tells systemd to keep your user’s services running, even when you are not logged in.
Your Step-by-Step Workflow
Here is the typical process for deploying a container as a rootless service:
- Install Podman: Make sure you have Podman version 4.4 or newer.
- Enable Linger (if rootless): Run
sudo loginctl enable-linger $USERso your services run after you log out. - Create Directory: Make the folder for your user’s Quadlet files:
mkdir -p ~/.config/containers/systemd. - Create Quadlet File: Write your service file (e.g.,
nano ~/.config/containers/systemd/myapp.container). Make sure to include an[Install]section so it starts on boot. - Reload Daemon: Run
systemctl --user daemon-reload. This is the most important step. It tells systemd to run the Podman generator, which finds your file and creates the realmyapp.service. - Start Service: You can now start your service:
systemctl --user start myapp.service. - Enable at Boot (Automatic): You don’t need to run
systemctl --user enable. If your file has an[Install]section (likeWantedBy=default.target), the daemon-reload step (Step 5) automatically enables it for you. - Manage: Your container is now a native service. Check its status with
systemctl --user status myapp.serviceand see its logs withjournalctl --user -u myapp.service.
3. Quadlet vs. Other Tools
Quadlet vs. The Old podman generate systemd Method
The podman generate systemd command is now deprecated, which means it is old and no longer recommended. All new development is focused on Quadlet.
Here is why Quadlet is better:
- Workflow:
generate systemdwas a manual, multi-step process (run container, generate file, copy file). Quadlet is declarative (create file, reload). - Maintenance:
generate systemdcreated static files that broke when Podman was updated. Quadlet’s generator model means your services are “self-healing” and always use the correct, up-to-date settings for your Podman version. - Simplicity: A Quadlet file is clean and easy to read. A generated
.servicefile was a “wall of text” full of complex commands that were hard to understand or change safely.
Quadlet vs. Docker Compose: Production Service vs. Development Tool
People often ask why Quadlet is needed when Docker Compose (or podman-compose) exists. They are built for different jobs.
Docker Compose / podman-compose: These tools are great for local development. They make it easy to spin up (docker compose up) and tear down (docker compose down) a full application environment for testing. But they aren’t meant to run production services. To do that, you have to wrap the docker compose command itself inside a systemd service, which is clumsy. Also, podman-compose is a separate, third-party tool and not the main focus for Podman.
Quadlet: This tool is designed specifically for running containers as production system services. Its “orchestration tool” is systemd. This direct systemd integration is Quadlet’s biggest advantage. A Quadlet service can use standard systemd rules (like After=, Requires=, Wants=) to create dependencies on any other service on the host, not just other containers. For example, you can make a container wait until After=nfs-mounts.target or Requires=my-native-database.service. Docker Compose has no idea about the host’s services and can’t do this. This makes Quadlet a much better and more reliable choice for single-node appliances, edge devices, and any system where containers must start in a specific order with other system processes.
Feature-by-Feature Comparison Table
| Feature | Quadlet | podman generate systemd | Docker / podman-compose |
|---|---|---|---|
| Primary Use Case | Production system services | (Old) One-time service creation | Local development |
| Model | Declarative (“what”) | Imperative (“how”) | Declarative (“what”) |
| Stays Up-to-Date? | Yes: Auto-updates on daemon-reload | No: Static files break on Podman updates | N/A: Self-contained, but not a service |
| Works with systemd? | Native: Uses systemd for all logic | Partial: Creates a static systemd file | No: Runs via a separate process |
| Dependency Mgt. | Full systemd: After=, Wants=, etc. | Full systemd: (If manually edited) | Isolated: depends_on (within Compose only) |
| Official Support | Yes: Core Podman feature | Deprecated: Bug-fix only | No: podman-compose is 3rd-party |
4. The Different Types of Quadlet Files
Quadlet uses different file extensions for different jobs. The generator reads all of them to build the final systemd services.
The Main File: .container Files
This is the most common file type. It defines how to run a single container as a service. You use a [Container] section to list podman run options.
A key feature is automatic dependencies. If your .container file includes a line like Network=my-net.network or Volume=my-data.volume:/data, the generator automatically adds After=my-net.service and Requires=my-data.service to the final service file.
Table 2: Key [Container] Section Options
| Key | Description |
|---|---|
Image= |
(Required) The container image to run. |
ContainerName= |
Sets the container name. Default: systemd-%N. |
Exec= |
The command to run in the container (replaces image CMD). |
PublishPort= |
Publishes a port to the host (e.g., 8080:80). |
Volume= |
Mounts a host path or named volume (e.g., my-vol.volume:/data:z). |
Network= |
Attaches to a custom network (e.g., my-net.network). |
Pod= |
Joins a Podman pod defined by a .pod file (e.g., my-pod.pod). |
AutoUpdate= |
Enables auto-updates (e.g., registry). |
Environment= |
Sets an environment variable (e.g., KEY=value). |
EnvironmentFile= |
Sets environment variables from a file. |
Secret= |
Mounts a Podman secret into the container. |
Label= |
Sets labels on the container. |
ReadOnly= |
Makes the container’s main file system read-only. |
User= |
The (numeric) UID to run as inside the container. |
PodmanArgs= |
“Escape hatch” to pass extra arguments directly to podman run. |
Persistent Storage: .volume Files
A .volume file defines a Podman named volume (a persistent storage space). The generator creates a simple “one-shot” service that runs podman volume create if the volume doesn’t already exist. This is very important for apps that need to save data. A .container file can then require this service to make sure the storage is ready before the container starts.
Table 3: Key [Volume] Section Options
| Key | Description |
|---|---|
VolumeName= |
Sets the name of the volume. Default: systemd-%N. |
Label= |
Sets labels on the volume. |
Driver= |
Specify the volume driver (e.g., image). |
Device= |
The path of a device to be mounted. |
Options= |
Mount options for the filesystem (e.g., o=XYZ). |
User= / Group= |
The host user/group (or name) to set as the owner of the volume. |
Copy= |
If true (default), copies content from the image path into the volume on first run. |
Custom Networking: .network Files
A .network file defines a Podman network. Just like .volume files, this creates a “one-shot” service that runs podman network create if the network doesn’t exist. This is needed for multi-container apps so they can be on their own private network and talk to each other using their container names as hostnames.
Table 4: Key [Network] Section Options
| Key | Description |
|---|---|
Driver= |
Sets the network driver (e.g., bridge). |
Subnet= |
The network’s IP range (e.g., 192.168.30.0/24). |
Gateway= |
The gateway for the subnet (e.g., 192.168.30.1). |
Label= |
Sets labels on the network. |
DisableDNS= |
Disables DNS for the network. |
Internal= |
Restricts network to internal-only communication. |
InterfaceName= |
Specifies the name of the network interface created on the host. |
Grouping Containers: .pod Files
Introduced in Podman 5.x, .pod files let you create a Podman pod. A pod is a group of containers that share resources, like the network, similar to a Kubernetes Pod. This file defines the pod’s shared settings, like ports for the whole group. Then, individual .container files can join the pod by using Pod=my-pod.pod. The generator automatically makes sure the pod service is started before any containers that are part of it.
Table 5: Key [Pod] Section Options
| Key | Description |
|---|---|
PodName= |
Sets the pod name. Default: systemd-%N. |
PublishPort= |
Publishes ports for the entire pod (e.g., 80:80). |
Network= |
Attaches the pod to a custom network. |
NetworkAlias= |
Adds a network-scoped alias for the pod. |
Label= |
Sets OCI labels on the pod. |
StopTimeout= |
Timeout in seconds to stop the pod. |
ExitPolicy= |
Policy for exiting the pod when containers stop (e.g., continue). |
Running Kubernetes Files: .kube Files
This very powerful file type lets systemd run an application defined in a standard Kubernetes YAML file. The service runs podman kube play to create all the pods, containers, and volumes from the YAML. This is a great alternative to podman-compose for complex apps, letting you use Kube-style files on a single machine. It’s also great for development: you can test a Kube YAML file locally with Quadlet before deploying the exact same file to a real Kubernetes cluster.
Table 6: Key [Kube] Section Options
| Key | Description |
|---|---|
Yaml= |
(Required) The path to the Kubernetes YAML file. |
Network= |
Attaches the Kube pod to a specific Podman network (e.g., my-net.network). |
PublishPort= |
Overrides or sets port mappings. |
ConfigMap= |
Loads a ConfigMap from an additional file. |
AutoUpdate= |
Enables auto-updates for the containers. |
LogDriver= |
Sets the log driver for the Kube pod. |
Build-on-Demand: .build Files
A .build file defines a service that builds a container image using podman build. This is a “one-shot” service. A .container file can then set its image as Image=my-app.build. This automatically creates a dependency, forcing systemd to run the build service before it tries to start the container. This is great for development or for edge devices that pull code and build images locally.
Table 7: Key [Build] Section Options
| Key | Description |
|---|---|
Containerfile= |
Path to the Containerfile or Dockerfile. Default: Dockerfile. |
Target= |
Sets the target build stage within the Containerfile. |
BuildArg= |
Sets a build-time variable (--build-arg). |
IgnoreFile= |
Specifies a custom .dockerignore file. |
Pulling Images: .image Files
A .image file makes sure a container image is pulled from a registry. This also creates a “one-shot” service that runs podman pull. This is useful for pulling large images at boot time, so they are already downloaded when your main container service needs to start.
Table 8: Key [Image] Section Options
| Key | Description |
|---|---|
Image= |
(Required) The full image name to pull. |
Policy= |
Pull policy (e.g., always, newer). |
AuthFile= |
Path to an authentication file for private registries. |
Creds= |
Credentials for a private registry (e.g., username:password). |
TLSVerify= |
Whether to verify TLS certificates (e.g., true, false). |
Managing Other Data: .artifact Files
The newest Quadlet file, .artifact, manages pulling OCI Artifacts. These are items in a registry that are not container images, such as WebAssembly modules, AI models, or config files. This file creates a service to pull them using podman artifact pull.
Table 9: Key [Artifact] Section Options
| Key | Description |
|---|---|
Artifact= |
(Required) The full artifact name to pull. |
AuthFile= / Creds= |
Authentication for the private registry. |
DecryptionKey= |
Key to decrypt the artifact. |
Retry= / RetryDelay= |
Retry logic for failed pulls. |
TLSVerify= |
Whether to verify TLS certificates. |
5. Practical Examples: Building Multi-Container Apps
How Dependencies Work (The Easy Way vs. The Manual Way)
Quadlet handles service dependencies in two ways:
- The Easy Way (Implicit): This works by file names. When your
.containerfile saysNetwork=my-net.networkorVolume=my-data.volume, Quadlet automatically addsRequires=my-net.serviceandAfter=my-net.serviceto the generated service file. - The Manual Way (Explicit): You can use standard systemd rules in the
[Unit]section of your file. By addingRequires=database.serviceandAfter=database.service, you can make your app container wait for another container service to be fully started.
Example 1: Simple Nginx Web Server
This example runs a single Nginx container as a rootless user. It will restart if it fails and start automatically on boot.
File: ~/.config/containers/systemd/nginx.container
[Unit]
Description=A simple Nginx web server
# Wait for the filesystem and network to be ready
After=local-fs.target network-online.target
Wants=network-online.target
[Container]
ContainerName=nginx-www
Image=docker.io/nginxinc/nginx-unprivileged:latest
PublishPort=8080:8080
# Mount a host directory as the website content
# 'ro' = read-only, 'z' = handle SELinux labeling
Volume=/srv/www:/usr/share/nginx/html:ro,z
# Pass-through to systemd: restart if it fails
Restart=always
[Install]
# This section enables the service on boot for the default target
WantedBy=default.target
Example 2: A Two-Tier WordPress Application (WordPress + MariaDB)
This example shows a full application with a database and a web app. It uses four files to create a network, a volume, the database container, and the web app container. It makes sure the database is running before the web app starts.
File 1: wordpress.network
Purpose: Creates a private network for the containers to talk to each other by name.
[Unit]
Description=Podman network for WordPress
[Network]
Label=app=wordpress
File 2: wordpress-db.volume
Purpose: Creates a persistent storage volume for the database files.
[Unit]
Description=Volume for WordPress DB
[Volume]
Label=app=wordpress
File 3: wordpress-db.container
Purpose: Runs the MariaDB database. It automatically depends on wordpress.network and wordpress-db.volume because the names match.
[Unit]
Description=WordPress Database Container (MariaDB)
[Container]
ContainerName=wordpress-db
Image=docker.io/library/mariadb:10
Network=wordpress.network
Volume=wordpress-db.volume:/var/lib/mysql:z
# Secrets should be passed via EnvironmentFile or Podman secrets
Environment=MARIADB_USER=wordpress
Environment=MARIADB_DATABASE=wordpress
Environment=MARIADB_RANDOM_ROOT_PASSWORD=1
Environment=MARIADB_PASSWORD=changeme
[Install]
WantedBy=default.target
File 4: wordpress-app.container
Purpose: Runs the WordPress app. This file shows the manual dependency on the database.
[Unit]
Description=WordPress App Container
# Explicit dependency: Do not start this container until
# the wordpress-db.service (from wordpress-db.container) is running.
Requires=wordpress-db.service
After=wordpress-db.service
[Container]
ContainerName=wordpress-app
Image=docker.io/library/wordpress:latest
Network=wordpress.network
PublishPort=8000:80
Environment=WORDPRESS_DB_HOST=wordpress-db
Environment=WORDPRESS_DB_USER=wordpress
Environment=WORDPRESS_DB_NAME=wordpress
Environment=WORDPRESS_DB_PASSWORD=changeme
[Install]
WantedBy=default.target
This setup defines the entire application, its resources, and its startup order, all managed by systemd.
6. Advanced Features and Management
How to Automatically Update Containers
Quadlet gives you a simple way to use Podman’s auto-update feature, which can pull new images and restart your services automatically.
How it works: The auto-update system has two parts:
- A systemd Timer: Podman includes
podman-auto-update.timer, which runs daily to check for updates. - A Label: The update command only checks containers that have the label
io.containers.autoupdateset.
The Quadlet Shortcut: Instead of making you remember that long label, Quadlet gives you a simple key: AutoUpdate=registry. When the generator sees this in your .container file, it automatically adds the correct Label=io.containers.autoupdate=registry to the final service file. For .kube files, it adds the same setting as a Kubernetes annotation.
To use this feature, just add AutoUpdate=registry to your .container file and then enable the timer one time: systemctl --user enable --now podman-auto-update.timer.
Help for Migrating: The podlet Tool
If you are moving from Docker Compose or the old podman generate systemd method, a helper tool called podlet can help.
podlet compose [docker-compose.yml]: This command reads adocker-compose.ymlfile and automatically generates the matching Quadlet files (.container,.network,.volume) for you.podlet generate container <name>: This command looks at a container you already have running and generates a.containerfile for it. This is the new replacement forpodman generate systemd.
New Commands: podman quadlet
With Podman 5.x, Quadlet is now a core part of Podman and has its own commands to make managing files easier:
podman quadlet list: Lists all Quadlet files on your system.podman quadlet print: Shows the contents of a Quadlet file.podman quadlet install: Installs a Quadlet file to the correct system folder.podman quadlet rm: Removes an installed Quadlet file.
7. Conclusion: The Future of Quadlet
Summary: The New Standard for Single-Node Apps
Podman Quadlet is now the official, recommended way to run containers as services on a Linux machine. It’s a “declarative” (tell it what you want) and reliable solution that completely replaces the old, “brittle” podman generate systemd method.
While it doesn’t replace Docker Compose for local development, it is a much better tool for running services in production on a single host. Its “secret weapon” is that it works directly with systemd, letting your containers use the host’s native dependency management, logging, and service controls.
What’s Next for Quadlet
The new file types added in Podman 5.x (like .pod, .build, .image, and .artifact) show a clear direction. Quadlet is growing beyond just running containers. It’s becoming a complete tool for managing the entire application lifecycle on a single node: from building images, to pulling data, to running complex multi-container apps.
This makes Quadlet the perfect choice for edge computing, embedded systems, and server “appliances.” These systems need to be reliable and simply defined, but are not big enough to need a full Kubernetes cluster. Quadlet fills this gap perfectly, providing a strong, self-healing, and native way to run container services on Linux.


