🎉 NEW YEAR SALE! 40% OFF on Annual Premium+ Plan - Till 31st Dec! Use SUPERSALE40 Shop Now →

Podman Quadlet: Run Containers as systemd Services on RHEL

Published On: 3 April 2026

Objective

If you're managing containers in production and you're still writing podman run commands in shell scripts wrapped in a hand-crafted systemd unit, you've probably felt the friction. The unit file grows, the restart logic gets complicated, dependencies between containers are messy to express, and half the time something subtle breaks after a reboot and you spend an hour figuring out why a container didn't come back up in the right order.

Podman Quadlet fixes this. It landed in Podman 4.4 and ships natively in RHEL 9.1 and RHEL 10. The idea is simple: you write a small, declarative file describing your container - image, ports, volumes, environment, restart policy - drop it in the right directory, and systemd generates a proper service unit automatically. From that point on, you use systemctl and journalctl like you would for any other service. No new tools to learn. No daemon running in the background. No Compose wrapper sitting between you and the OS.

This guide covers the whole thing: how Quadlet works, all six unit file types, real-world examples including rootless containers, multi-container pods, secret management, and what you need to know for production.

What Quadlet Actually Is ?

Quadlet is a systemd generator. That's a specific thing in the systemd world - a program that runs early in the boot process and produces unit files on the fly before systemd starts bringing services up. When you place a .container file (or .pod, .volume, .network, .image, or .kube) in the right directory, systemd finds it, calls the Quadlet generator, and the generator spits out a complete .service unit. Systemd then manages that service like anything else.

"One thing worth being clear about: Quadlet doesn't replace Podman." Podman still does the actual container work. Quadlet just bridges Podman and systemd, so systemd handles lifecycle, dependency ordering, logging, and restart behavior. If you know systemd, you already know how to manage a Quadlet-based container.

How It Compares to the Alternatives

Feature Docker Compose Systemd Unit (manual) Podman Quadlet
Rootless support Limited Complex Native
Systemd integration External wrapper Manual Native, automatic
Auto-start on boot Requires extras Manual setup Built-in
Dependency ordering Limited Manual Declarative
Secret management Env vars Manual Podman secrets
Healthcheck + restart Yes Manual ExecStart Built-in
Pod grouping via Compose Complex Native .pod files
Daemon required Yes (dockerd) No No
RHEL support Extra install Built-in RHEL 9.1+ / 10

 

The old podman generate systemd approach generated static unit files from running containers. It worked, but it was fragile - you'd generate the file once, the container config would drift, and the unit file wouldn't reflect reality anymore. Quadlet regenerates the service definition fresh at every boot from your source file. It's declarative rather than imperative, which is how it should work.

Version and Availability

Quadlet is included in the podman package starting with version 4.4. RHEL 9.1 ships it, RHEL 10 ships it. Nothing extra to install. Verify you're on a recent enough version:

podman -version
# Quadlet requires Podman 4.4 or later

RHEL 8 doesn't have it. If you're still on RHEL 8 and need this functionality, podman generate systemd is the fallback - but the real answer is upgrading to 9.1 or later.

The Six Unit File Types

 

Extension What It Does Typical Use
.container Run a single container Web server, database, app service
.pod Group containers into a pod Multi-container apps sharing a network
.volume Declare a named Podman volume Persistent data storage
.network Declare a Podman network Custom container networking
.image Pull and manage an image Pre-pulling images at boot
.kube Deploy a Kubernetes YAML manifest Migrating from Kubernetes

 

Where to Put Your Files

For system-wide (root) containers:

/etc/containers/systemd/
# or for vendor/package-provided units
/usr/share/containers/systemd/

For rootless (per-user) containers:

~/.config/containers/systemd/

Rootless is the recommended approach on RHEL for anything that doesn't need elevated privileges. Rootless Quadlet services run as your user and are managed with systemctl -user. They start when you log in - or at boot if you enable lingering, which you almost certainly want for server workloads:

loginctl enable-linger $USER

Your First Quadlet Container: Nginx

Here's a complete working example. We'll run Nginx as a rootless systemd service.

Step 1: Create the unit file

mkdir -p ~/.config/containers/systemd/
vi ~/.config/containers/systemd/nginx.container
[Unit]
Description=Nginx Web Server
After=network-online.target

[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
Volume=%h/html:/usr/share/nginx/html:Z
Environment=NGINX_HOST=localhost
ContainerName=nginx-web

[Service]
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

Step 2: Create the web root

mkdir -p ~/html
echo '<h1>Hello from Podman Quadlet!</h1>' > ~/html/index.html

Step 3: Reload systemd and start

systemctl -user daemon-reload
systemctl -user start nginx.service

Step 4: Verify and enable at boot

systemctl -user status nginx.service
systemctl -user enable nginx.service

# Test it
curl http://localhost:8080

Notice you never wrote a .service file. Quadlet named the generated service after your unit file - nginx.container becomes nginx.service. That's how it works every time.

The [Container] Section: Options Worth Knowing

Image and pull policy

[Container]
Image=registry.access.redhat.com/ubi9/ubi:latest
# missing (default), always, never, newer
Pull=newer

Networking

PublishPort=443:443
PublishPort=80:80
# Attach to a named network defined in a .network file
Network=myapp.network

Volumes

# Host directory mount - always use :Z on RHEL for SELinux
Volume=/data/app:/app/data:Z
# Named volume from a .volume Quadlet file
Volume=app-data.volume:/var/lib/app:Z

Environment and secrets

Environment=APP_ENV=production
EnvironmentFile=/etc/myapp/env
Secret=db-password,type=env,target=DB_PASSWORD

Security and resource limits

User=1001
NoNewPrivileges=true
ReadOnly=true
PodmanArgs=-memory=512m -cpus=1.0

Health checks

HealthCmd=CMD-SHELL curl -f http://localhost/ || exit 1
HealthInterval=30s
HealthRetries=3
HealthStartPeriod=10s

Persistent Storage with .volume Files

Rather than bind-mounting host directories, define named volumes as their own Quadlet unit. It's cleaner, more portable, and Quadlet handles the dependency ordering automatically - you don't need to add After= or Requires= to wire them together.

# ~/.config/containers/systemd/app-data.volume

[Volume]
Label=app=myapp
Label=env=production

Reference it in your container file:

Volume=app-data.volume:/var/lib/myapp:Z

Custom Networking with .network Files

If you have containers that need to talk to each other without exposing ports to the host, define a shared network. Containers on the same Quadlet network can reach each other by container name, the same way Docker Compose service names work.

# ~/.config/containers/systemd/myapp.network

[Network]
Subnet=10.89.1.0/24
Gateway=10.89.1.1
Label=project=myapp

Then in each container that should share this network:

Network=myapp.network

Multi-Container Pods with .pod Files

A Podman pod groups containers that share a network namespace - same idea as a Kubernetes Pod. Use this when containers genuinely need to communicate over localhost, or when you want them treated as a single deployable unit.

Define the pod

# ~/.config/containers/systemd/webapp.pod

[Pod]
PublishPort=8080:80

App container

# ~/.config/containers/systemd/webapp-app.container

[Unit]
Description=Web Application

[Container]
Image=registry.example.com/myapp:latest
Pod=webapp.pod
Volume=app-data.volume:/app/data:Z

[Service]
Restart=always

[Install]
WantedBy=default.target

Redis sidecar

# ~/.config/containers/systemd/webapp-redis.container

[Unit]
Description=Redis Cache for Web Application

[Container]
Image=docker.io/library/redis:7-alpine
Pod=webapp.pod

[Service]
Restart=always

[Install]
WantedBy=default.target

Because they share the pod's network namespace, the app container reaches Redis at localhost:6379. Quadlet creates pod-webapp.service automatically and makes both containers depend on it - no manual wiring needed.

Deploying Kubernetes YAML with .kube Files

Already have Kubernetes manifests? The .kube file type lets you deploy them directly with Podman, no cluster required. This is useful for teams migrating workloads from OpenShift or Kubernetes to standalone RHEL servers, and for developers who want to run against a real manifest locally without spinning up a cluster.

# ~/.config/containers/systemd/myapp.kube

[Kube]
Yaml=/etc/myapp/deployment.yaml

[Service]
Restart=always

[Install]
WantedBy=default.target

Secret Management: Do It Right

Don't put passwords or API keys in your unit files. Podman has a secret store built in - use it.

Create secrets

# From a string
echo -n 'mysecretpassword' | podman secret create db-password -

# From a file
podman secret create tls-cert /path/to/cert.pem

# List what's stored
podman secret ls

Reference in a .container file

[Container]
Image=docker.io/library/postgres:16

# Mount as a file at /run/secrets/db-password
Secret=db-password

# Or inject as an environment variable
Secret=db-password,type=env,target=POSTGRES_PASSWORD

Secrets stored this way never show up in the unit file, the process list, or podman inspect output as plaintext. That's the point.

Day-to-Day systemd Commands

Always reload after editing unit files

systemctl -user daemon-reload

Start, stop, restart

systemctl -user start nginx.service
systemctl -user stop nginx.service
systemctl -user restart nginx.service

View logs

# Live follow
journalctl -user -u nginx.service -f

# Last hour
journalctl -user -u nginx.service -since '1 hour ago'

Inspect what Quadlet actually generated

systemctl -user cat nginx.service

Debug Quadlet generation without applying it

# For system units
/usr/lib/systemd/system-generators/podman-system-generator -dryrun

# For user units
QUADLET_UNIT_DIRS=~/.config/containers/systemd /usr/lib/systemd/user-generators/podman-user-generator -dryrun

SELinux and Volume Mounts on RHEL

RHEL runs SELinux in enforcing mode. If you mount a host directory into a container without the right SELinux label, access gets blocked - and the error messages aren't always obvious about why. The fix is simple: always add :Z or :z to your volume mounts.

# :Z - private label, for a single container
Volume=/home/user/data:/app/data:Z

# :z - shared label, for multiple containers sharing the same volume
Volume=/shared/data:/app/data:z

If something still isn't working, check for denials and fix the context:

sudo ausearch -m avc -ts recent | grep podman
sudo restorecon -Rv /path/to/volume

Firewall Configuration

Publishing a port in your Quadlet file doesn't automatically open it in firewalld. Do that separately:

sudo firewall-cmd -permanent -add-port=8080/tcp
sudo firewall-cmd -reload

For rootless containers, stick to ports above 1024 unless you've adjusted net.ipv4.ip_unprivileged_port_start. It's less hassle than fighting the kernel over privileged port binding.

Automatic Image Updates

Quadlet integrates with podman auto-update. Add a label to your container and the auto-update timer handles the rest:

[Container]
Image=docker.io/library/nginx:latest
Label=io.containers.autoupdate=registry
systemctl -user enable -now podman-auto-update.timer

The timer runs daily by default, checks the registry for newer images, pulls them, and restarts affected containers. Good for keeping non-critical services patched without manual work. For production services where you want control over what version runs, pin the image tag explicitly and don't use this.

Troubleshooting

Service won't start after daemon-reload

systemctl -user status nginx.service
journalctl -user -u nginx.service -n 50

Quadlet file isn't being picked up

Check the file extension and directory, then reload:

ls -la ~/.config/containers/systemd/
systemctl -user daemon-reload

Volume permission denied

Almost always SELinux. Add :Z to the mount and make sure the directory exists with the right ownership:

chmod 755 ~/mydata
# Then in your unit file:
# Volume=%h/mydata:/data:Z

Container starts then exits immediately

journalctl -user -u mycontainer.service -no-pager
podman logs mycontainer

Port already in use

ss -tlnp | grep 8080

Production Checklist

  • Run rootlessdedicated non-root user per service, least privilege by default
  • Pin image versions image:1.25.3 not image:latest in production; surprises are bad
  • Use Podman secrets no passwords in unit files or environment variables in plaintext
  • Always add :Z to volume mounts saves a lot of SELinux debugging later
  • Set Restart=always with RestartSec containers need to come back after transient failures
  • Add health checks gives systemd real signal on whether the container is actually working
  • Use .network files isolate container communication, don't leave everything on the default bridge
  • Enable lingering rootless services need loginctl enable-linger to survive reboots without an interactive session
  • Review generated unit files systemctl -user cat shows exactly what Quadlet produced; audit it periodically
  • Enable auto-update selectively useful for non-critical services, risky for anything where the image version matters

Conclusion

Quadlet is one of those features that seems small until you've actually used it. Writing a 20-line .container file and getting a fully managed systemd service out of it - with proper restart behavior, logging via journald, dependency ordering, and SELinux integration - is a genuinely better experience than any of the alternatives that came before it. If you're still writing podman generate systemd output into hand-maintained unit files, stop. If you're wrapping Compose in a systemd unit, stop. Quadlet handles both cases more cleanly, it's the supported path on RHEL going forward, and it plays properly with the rest of the OS in a way that bolt-on solutions never quite did. Drop your .container files in the right directory, run daemon-reload, and let systemd take it from there.