Podman Quadlets on Linux: Systemd-Native Rootless Containers in 2026

Podman Quadlets turn rootless containers into first-class systemd services. Learn install, hardening, auto-updates, and troubleshooting on Linux in 2026.

Podman Quadlets: Systemd Rootless Guide 2026

Updated: September 1, 2026

Podman Quadlets are systemd unit files that describe rootless or rootful containers, pods, volumes, and networks in a declarative .container/.pod/.kube format. A systemd generator translates them at boot into standard .service units, so every container becomes a first-class systemd citizen with journald logs, cgroup limits, and dependency ordering. Introduced in Podman 4.4 and stabilized through Podman 5.x, Quadlets replace the fragile podman generate systemd workflow and provide a lighter alternative to docker-compose on hardened Linux servers.

  • Quadlets are declarative systemd units (.container, .pod, .volume, .network, .kube, .image, .build) processed by the podman-system-generator at boot.
  • Rootless Quadlets live in ~/.config/containers/systemd/; rootful units live in /etc/containers/systemd/ or /usr/share/containers/systemd/.
  • Because Quadlets emit real .service units, they inherit every systemd sandbox directive: NoNewPrivileges=, ProtectSystem=, per-service cgroup limits, and journald integration.
  • podman-auto-update.timer plus the AutoUpdate=registry label safely rolls containers forward and rolls back on health-check failure.
  • Quadlets require Podman 4.4+ (5.x recommended) and systemd 250+; on rootless installs you must enable user lingering with loginctl enable-linger $USER.
  • Quadlets are more secure and less magic than docker-compose, but pod-scoped and single-host. For multi-host orchestration use Kubernetes YAML via .kube or Nomad.

What are Podman Quadlets?

A Podman Quadlet is a small INI-style file (syntactically a systemd unit) that describes a container workload declaratively. Instead of hand-writing a .service that runs podman run with two dozen flags, you write a myapp.container file with an [Container] section, drop it in a known directory, and let the podman-system-generator translate it into a native .service unit the next time systemd reloads. The generator lives at /usr/lib/systemd/system-generators/podman-system-generator and runs every time you call systemctl daemon-reload.

This matters because the earlier podman generate systemd command produced snapshot unit files that had to be regenerated whenever the container spec changed. That workflow quietly rotted in production, and it was deprecated in Podman 5.0. Quadlets flip the model: your source of truth is the Quadlet file, and the resulting unit is always freshly generated.

What do you get in return? Automatic journald logging (journalctl -u myapp.service), proper dependency ordering with After=/Requires=, cgroup accounting per container, and systemd's socket activation, timers, and sandboxing knobs, all in one place. Honestly, once you've shipped a stack this way, going back to compose feels like a step backwards.

The formal spec covers seven unit types today: .container (single container), .pod (pod holding multiple containers), .volume, .network, .kube (deploy a Kubernetes YAML), .image (pre-pull an image on boot), and .build (build an image from a Containerfile). Each maps to one generated systemd unit whose name is derived from the file. For example, web.container generates web.service.

Installing and enabling Quadlets on Linux

Quadlets ship with Podman itself, so there's no separate package to install. You need Podman 4.4 or later for basic support, but I strongly recommend Podman 5.2+ (Fedora 40+, RHEL 9.4+, Ubuntu 24.04 backports, Debian 13 trixie) because it stabilized the .pod generator, added .build, and removed the last quirks of the rootless network stack (pasta replaces slirp4netns as the default). Verify the components on your host before you write your first Quadlet:

# Check Podman version and generator presence
podman --version                                    # want 4.4+, ideally 5.2+
ls /usr/lib/systemd/system-generators/podman-system-generator
systemctl --version | head -1                       # want systemd 250+

# On rootless installs verify the user generator too
ls /usr/lib/systemd/user-generators/podman-user-generator

Distros:

  • Fedora / RHEL / CentOS Stream: sudo dnf install podman pulls both generators; SELinux policies for container_t are already in place.
  • Ubuntu 24.04 / 24.10 / Debian 13: sudo apt install podman then confirm the generator path. Older Ubuntus (22.04) shipped Podman 3.x, which does not support Quadlets. You'll need to add the official installation sources or use the Kubic project builds.
  • Arch / openSUSE Tumbleweed: current Podman is always Quadlet-capable.

For rootless usage, one more preparation step is required. The user's systemd instance normally exits when they log out, which kills every rootless container. Enable lingering so services keep running:

# Persist the user's systemd manager across logouts
loginctl enable-linger $USER

# Verify
loginctl show-user $USER | grep Linger              # Linger=yes

Writing your first .container Quadlet

Let's ship a hardened rootless nginx serving a static site from a bind mount, listening on port 8080. Create the Quadlet directory and file:

mkdir -p ~/.config/containers/systemd
$EDITOR ~/.config/containers/systemd/web.container

Contents of web.container:

[Unit]
Description=Rootless nginx serving /srv/www
Wants=network-online.target
After=network-online.target

[Container]
Image=docker.io/library/nginx:1.27-alpine
ContainerName=web
PublishPort=127.0.0.1:8080:80
Volume=%h/srv/www:/usr/share/nginx/html:ro,Z
Environment=NGINX_ENTRYPOINT_QUIET_LOGS=1
# Security posture
NoNewPrivileges=true
DropCapability=ALL
AddCapability=CHOWN,SETGID,SETUID,NET_BIND_SERVICE
ReadOnly=true
Tmpfs=/tmp:rw,size=16m,mode=1777
Tmpfs=/var/cache/nginx:rw,size=64m
Tmpfs=/var/run:rw,size=1m
SecurityLabelType=container_t
# Health check
HealthCmd=curl -fsS http://127.0.0.1:80/ || exit 1
HealthInterval=30s
HealthRetries=3

[Service]
Restart=always
TimeoutStartSec=120

[Install]
WantedBy=default.target

Reload the user systemd instance so the generator picks up the file, then start the service:

systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service
curl -I http://127.0.0.1:8080/                      # HTTP/1.1 200 OK

Note that you refer to the generated unit by its .service name, not the source file. The Quadlet spec derives web.service from web.container. Because the [Install] block requests WantedBy=default.target, running systemctl --user enable web.service will auto-start the container at login (or at boot, thanks to linger).

The full Quadlet type reference

Beyond .container, the Quadlet generator understands six sibling types. Each generates its own systemd unit, and you reference other Quadlets by their unit name inside directives (Volume=data.volume:/data, Network=frontend.network). This is far cleaner than the raw podman network create and podman volume create commands scattered across ad-hoc scripts.

Quadlet typePurposeGenerated unitCommon directives
.containerRun a single container<name>.serviceImage, PublishPort, Volume, Network, Environment
.podGroup containers into a pod (shared netns/IPC)<name>-pod.servicePodName, PublishPort, Network
.volumeNamed volume with driver/labels<name>-volume.serviceDriver, Label, Options, User, Group
.networkNamed Podman network<name>-network.serviceSubnet, Gateway, DNS, Internal
.kubeDeploy a Kubernetes YAML manifest<name>.serviceYaml, ConfigMap, Network
.imagePull an image on boot (dependency of others)<name>-image.serviceImage, Arch, AuthFile
.buildBuild an image from a local Containerfile<name>-build.serviceImageTag, SetWorkingDirectory, File

A typical multi-tier setup uses one of each. For a Grafana + Postgres stack you'd create monitor.pod, grafana.container, postgres.container, pgdata.volume, and reference them together:

# pgdata.volume: encrypted, labeled volume
[Volume]
Driver=local
Label=app=monitor
Options=type=ext4

# postgres.container references pgdata.volume and the monitor pod
[Container]
Image=docker.io/library/postgres:16-alpine
Pod=monitor.pod
Volume=pgdata.volume:/var/lib/postgresql/data:Z
Environment=POSTGRES_PASSWORD_FILE=%t/postgres.pw
Secret=pg_password,type=mount,target=/run/postgres.pw

Note the Secret= directive: Quadlets integrate cleanly with podman secret, so credentials never appear in the unit file. For deeper credential patterns see our Linux secrets management guide.

Rootless Quadlets, user linger, and subuid/subgid

The biggest security win of Quadlets on modern Linux is that rootless is the default recommendation. A rootless container runs entirely inside a user namespace: the container's root UID maps to your unprivileged host UID via subordinate UID/GID ranges in /etc/subuid and /etc/subgid. Even if an attacker escapes the container, they land as a nobody user on the host with no privileges. This is the same principle behind the sandboxed container runtimes we compared earlier, but built into stock Linux with no extra hypervisor.

Prerequisites for smooth rootless Quadlets:

# 1. Verify your user has a subuid/subgid range (usually pre-provisioned)
grep $USER /etc/subuid /etc/subgid
# amir:100000:65536
# amir:100000:65536

# 2. Ensure cgroups v2 is unified (Fedora/Ubuntu 22+ default; RHEL 9 needs boot flag)
mount | grep cgroup2                                # cgroup2 on /sys/fs/cgroup

# 3. Enable resource delegation for the user (allows per-container limits)
mkdir -p /etc/systemd/system/[email protected]
cat <<'EOF' | sudo tee /etc/systemd/system/[email protected]/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
EOF
sudo systemctl daemon-reload

Without Delegate=, your rootless Quadlets can start, but per-container MemoryMax= and CPUQuota= in the [Service] section silently no-op. Rootful Quadlets living in /etc/containers/systemd/ don't have this restriction, but you lose the user-namespace isolation. On multi-tenant hosts I always prefer rootless plus delegation. I got burned by a silent no-op once on a QA box, and it took an embarrassing hour of head-scratching before I found the missing drop-in.

Hardening Quadlets with SELinux, seccomp, and capabilities

Because a Quadlet becomes a real systemd service, every sandboxing directive from the systemd manual is at your disposal. Combine them with Podman's own security flags to get defense in depth:

[Container]
Image=ghcr.io/example/api:1.4.2
# Podman-level hardening
NoNewPrivileges=true
DropCapability=ALL
AddCapability=NET_BIND_SERVICE
ReadOnly=true
Tmpfs=/tmp:rw,size=8m
SecurityLabelType=container_t                       # SELinux domain
SeccompProfile=/etc/containers/seccomp/api.json     # custom seccomp
User=10001:10001                                    # non-zero UID inside container

[Service]
# systemd-level hardening on the wrapping .service
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
MemoryMax=512M
CPUQuota=200%
TasksMax=256

The SecurityLabelType= maps to the --security-opt label=type: flag and pins the container to a specific SELinux type. That's useful when you've written custom policy for a particular workload. For a broader treatment see our SELinux and AppArmor hardening guide. For seccomp profile design, start from the Podman default and remove syscalls the workload doesn't need. The strace -c -f -e trace=%all output on a production run is your best oracle.

One under-used directive is UserNS=auto:size=65536, which allocates a private user namespace per container instead of sharing the caller's mapping. This means two containers each get their own subuid slice, and a hypothetical CVE-2024-21626-style escape from one cannot see UIDs owned by the other on the host filesystem.

Auto-updates and rollback with podman-auto-update

Long-running containers need patching, but you don't want a broken image to leave the service down. Podman ships a two-piece auto-update mechanism that Quadlets integrate with by adding one label:

[Container]
Image=ghcr.io/example/api:1
Label=io.containers.autoupdate=registry

Then enable the timer:

# Rootless
systemctl --user enable --now podman-auto-update.timer

# Rootful
sudo systemctl enable --now podman-auto-update.timer

The timer fires daily by default. It queries the registry for a new digest of the currently pinned tag; if one exists, it restarts the container with the new image. Crucially, Podman records the previous image and, if the HealthCmd= health check fails after the restart, automatically rolls back. The podman auto-update --dry-run command previews what would change without pulling anything. Run it in CI to catch surprise upgrades before they hit production.

Prefer Label=io.containers.autoupdate=local when you build images on the host with .build Quadlets and want the timer to notice new local tags. For strictly pinned deployments, omit the label entirely and drive updates from your CI/CD pipeline, a topic we cover in the Linux CI/CD hardening guide.

Quadlets vs docker-compose vs Kubernetes: when to choose what

Quadlets sit between docker-compose and Kubernetes on the complexity/capability curve. They're single-host by design (a Quadlet describes state on the machine where it lives), but they gain systemd's dependency graph, cgroups accounting, and journald plumbing for free. Choose based on the operational blast radius you actually need.

DimensionPodman Quadletsdocker-composeKubernetes
ScopeSingle hostSingle hostMulti-host cluster
Rootless by defaultYesNo (rootless-docker is opt-in)Node runtime dependent
Config formatINI (systemd)YAMLYAML
Init system integrationNative systemd unitsExternal daemonOwn scheduler
Auto-updatesBuilt-in via timerManual / WatchtowerOperators / GitOps
Secretspodman secret, systemd-creds.env, externalK8s Secrets, external stores
Learning curveLow (if you know systemd)Very lowHigh
Multi-hostNoNoYes

My rule of thumb: for a single VM or bare-metal box running a handful of services, Quadlets beat compose on every axis that matters for a hardened Linux server. Unit files live in /etc where your configuration management already reaches, journald replaces a stack of ad-hoc log configs, and you never need to install the Docker daemon at all. For fleet management or anything requiring rolling deploys across nodes, jump to Kubernetes (and consider immutable node OSes as covered in our Talos vs Bottlerocket comparison).

If you already have a Kubernetes YAML you like, don't rewrite it. Drop it in a .kube Quadlet and Podman will run the manifest locally via podman kube play. This is a great migration path off compose files: podman kube generate --type deployment turns a compose stack into a K8s YAML you can then wrap in one .kube Quadlet.

Troubleshooting Quadlets

When a Quadlet won't start, the failure mode is almost always in one of three places: the generator didn't run, the generator produced a broken unit, or the container itself is crashing. Walk them in order.

Did the generator run? Every systemctl daemon-reload re-runs it. Confirm the output unit exists in the transient location:

# Rootless
ls /run/user/$UID/systemd/generator/web.service

# Rootful
sudo ls /run/systemd/generator/web.service

If the file is missing, the generator rejected your Quadlet, usually a typo in a section name or a directive. Invoke the generator manually to see the error inline:

# Rootless
/usr/lib/systemd/user-generators/podman-user-generator /tmp/qg /tmp/qg /tmp/qg
# Look at stderr for the specific rejection reason

Is the generated unit healthy? Inspect it:

systemctl --user cat web.service                    # see the resolved ExecStart
systemctl --user status web.service                 # exit code / last logs
journalctl --user -u web.service -n 100 --no-pager

Is the container itself failing? Because the wrapper unit runs podman run, you can copy the ExecStart line and run it interactively with -it --rm substituted for --detach, which prints the container's stdout/stderr directly. Common causes: SELinux relabel failures on bind-mounted volumes (add :Z), missing subuid/subgid entries, or a User= that doesn't exist in the image.

Frequently Asked Questions

Do Podman Quadlets replace docker-compose?

For single-host workloads, yes. Quadlets give you declarative container definitions, dependency ordering, journald logs, cgroup limits, and rootless-by-default in one systemd-native format. Docker-compose remains simpler for developers who only know YAML, but on a hardened Linux server Quadlets are the better production choice.

Which Podman version added Quadlets?

Quadlets shipped as tech preview in Podman 4.4 (January 2023), stabilized in Podman 4.6, and gained .pod, .image, and .build types across the Podman 5.x line. For production use in 2026 aim for Podman 5.2 or newer.

Can I run Quadlets rootless?

Yes. Place the file under ~/.config/containers/systemd/ and manage the service with systemctl --user. Enable loginctl enable-linger $USER so the container survives logouts, and add Delegate= to [email protected] if you want per-container cgroup limits.

How do Quadlets handle secrets?

The Secret= directive mounts entries from podman secret as files or environment variables. For cross-service secret distribution you can combine Quadlets with systemd-creds or an external Vault-style store, keeping credentials out of the Quadlet file itself.

Do I need SELinux for Quadlets?

No, but it helps. On Fedora/RHEL you get container isolation via the container_t domain automatically; the SecurityLabelType= directive lets you pin a custom label. On Ubuntu/Debian you rely on AppArmor plus the default seccomp filter, which is still strong, though SELinux gives you finer-grained type enforcement.

About the Author Editorial Team

Our team of expert writers and editors.