Tao
Tao

Podman Quadlet: A Simpler Way to Run Containers with systemd

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.

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 .service file 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.

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:

ini

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:

text

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.

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:

  1. Scan: It looks in specific folders (see section 2.2) for files ending in .container, .volume, .network, .pod, .kube, .build, .image, or .artifact.
  2. 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]).
  3. Generate: It translates your file into a full systemd .service file (e.g., myapp.service) and saves it in a temporary generator directory (like /run/systemd/generator/ or ~/.config/systemd/user/generator/).
  4. Load: systemd then reads the new myapp.service from that temporary directory and loads it, just like any other service file.

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.

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

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:

bash

sudo loginctl enable-linger <username>

This tells systemd to keep your user’s services running, even when you are not logged in.

Here is the typical process for deploying a container as a rootless service:

  1. Install Podman: Make sure you have Podman version 4.4 or newer.
  2. Enable Linger (if rootless): Run sudo loginctl enable-linger $USER so your services run after you log out.
  3. Create Directory: Make the folder for your user’s Quadlet files: mkdir -p ~/.config/containers/systemd.
  4. 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.
  5. 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 real myapp.service.
  6. Start Service: You can now start your service: systemctl --user start myapp.service.
  7. Enable at Boot (Automatic): You don’t need to run systemctl --user enable. If your file has an [Install] section (like WantedBy=default.target), the daemon-reload step (Step 5) automatically enables it for you.
  8. Manage: Your container is now a native service. Check its status with systemctl --user status myapp.service and see its logs with journalctl --user -u myapp.service.

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 systemd was a manual, multi-step process (run container, generate file, copy file). Quadlet is declarative (create file, reload).
  • Maintenance: generate systemd created 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 .service file was a “wall of text” full of complex commands that were hard to understand or change safely.

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 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

Quadlet uses different file extensions for different jobs. The generator reads all of them to build the final systemd services.

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.

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.

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.

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).

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.

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.

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).

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.

Quadlet handles service dependencies in two ways:

  1. The Easy Way (Implicit): This works by file names. When your .container file says Network=my-net.network or Volume=my-data.volume, Quadlet automatically adds Requires=my-net.service and After=my-net.service to the generated service file.
  2. The Manual Way (Explicit): You can use standard systemd rules in the [Unit] section of your file. By adding Requires=database.service and After=database.service, you can make your app container wait for another container service to be fully started.

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

ini

[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

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.

ini

[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.

ini

[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.

ini

[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.

ini

[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.

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:

  1. A systemd Timer: Podman includes podman-auto-update.timer, which runs daily to check for updates.
  2. A Label: The update command only checks containers that have the label io.containers.autoupdate set.

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.

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 a docker-compose.yml file 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 .container file for it. This is the new replacement for podman generate systemd.

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.

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.

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.

Related Content