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 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:
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 type
Purpose
Generated unit
Common directives
.container
Run a single container
<name>.service
Image, PublishPort, Volume, Network, Environment
.pod
Group containers into a pod (shared netns/IPC)
<name>-pod.service
PodName, PublishPort, Network
.volume
Named volume with driver/labels
<name>-volume.service
Driver, Label, Options, User, Group
.network
Named Podman network
<name>-network.service
Subnet, Gateway, DNS, Internal
.kube
Deploy a Kubernetes YAML manifest
<name>.service
Yaml, ConfigMap, Network
.image
Pull an image on boot (dependency of others)
<name>-image.service
Image, Arch, AuthFile
.build
Build an image from a local Containerfile
<name>-build.service
ImageTag, 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:
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:
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.
Dimension
Podman Quadlets
docker-compose
Kubernetes
Scope
Single host
Single host
Multi-host cluster
Rootless by default
Yes
No (rootless-docker is opt-in)
Node runtime dependent
Config format
INI (systemd)
YAML
YAML
Init system integration
Native systemd units
External daemon
Own scheduler
Auto-updates
Built-in via timer
Manual / Watchtower
Operators / GitOps
Secrets
podman secret, systemd-creds
.env, external
K8s Secrets, external stores
Learning curve
Low (if you know systemd)
Very low
High
Multi-host
No
No
Yes
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.
Bootc packages a whole Linux OS as an OCI image, then upgrades RHEL and Fedora servers transactionally with signed digests and automatic rollback. A 2026 walkthrough for building, signing, and deploying image-mode servers you can actually roll back safely.
Sigma turns Linux auditd, journald, sshd, and sudo events into portable YAML detections that compile to any SIEM. Learn pySigma, sigma-cli, Zircolite, and CI patterns for detection-as-code in 2026.
Install Zeek 7.x on Linux, tune AF_PACKET for line-rate capture, write your first detection script, use JA4 TLS fingerprints, and stitch Zeek into a SIEM for real threat hunting.