| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64 |
| Applies to | Any Linux host with Podman 5.x and systemd |
| Privilege | sudo for rootful examples on the lab host |
| Scope | Quadlet .pod units and how .container files attach with Pod= — multi-container application wiring, StartWithPod=, automatic and manual dependencies, pod-level ports and user namespaces, ExitPolicy, cgroup mode interaction, and the clean-exit restart trap. Assumes you already understand Podman pods and Quadlet basics. Does not cover generic pod concepts, full .container directive reference, or Kubernetes YAML. |
You already know a Podman pod groups containers that share a network namespace. Quadlet adds a .pod file type so systemd owns that pod declaratively, and member .container units join through Pod=. This guide builds a complete five-file application — one network, one volume, one pod, and two containers — then walks dependency translation, StartWithPod=, and the restart trap that catches production stacks when every container exits cleanly.
Podman version requirement
.pod Quadlet units need modern Podman 5.x. I tested every command on Podman 5.8.2 on RHEL 10.2. Before copying these examples on an older host, confirm your build actually ships pod Quadlet support:
podman --versionSample output:
podman version 5.8.2If your package is below Podman 5.0, upgrade or verify podman-system-generator lists .pod among supported unit types after systemctl daemon-reload. The rest of this guide assumes a 5.x generator that understands [Pod] sections.
What is a Quadlet .pod unit?
A .pod file tells Quadlet to create and manage a Podman pod through systemd. The filename drives the service name:
app.pod → app-pod.serviceUnless you override it, the Podman pod itself is named systemd-app:
[Pod]
PodName=quadlet-appContainers attach by referencing the source file, not the runtime pod name:
[Container]
Pod=app.podQuadlet translates Pod=app.pod into --pod quadlet-app on podman run and wires systemd dependencies between web.service and app-pod.service. You get pod lifecycle and service ordering from one declaration instead of hand-maintaining Requires= lines for every member.
Build a complete multi-container Quadlet application
The lab application uses four Quadlet resource types. Network and volume are prerequisites; the pod sits in the middle; containers hang off the pod; the volume mounts into one member:
app.network
│
▼
app.pod ──────┬── web.container
└── sidecar.container
app-data.volume
│
└── web.containerAll files live under /etc/containers/systemd/ on a rootful host. The same keys work in rootless Quadlet paths with systemctl --user.
Create the .network file
The pod attaches to a user-defined network declared in its own Quadlet file. Keep network-specific directives in the network unit — this article only needs the name:
[Network]
NetworkName=quadlet-appSave as app.network. Quadlet generates app-network.service and creates the network before the pod starts. See Create Podman networks when you need subnets, DNS, or driver options.
Create the .volume file
Persistent data for the web member uses a named volume:
[Volume]
VolumeName=quadlet-app-dataSave as app-data.volume. The web container references this file in its Volume= line so Quadlet can infer Requires=app-data-volume.service automatically.
Create the .pod file
The pod owns shared networking and host port publishing:
[Unit]
Description=Application pod
[Pod]
PodName=quadlet-app
Network=app.network
PublishPort=18080:8080
[Service]
Restart=always
[Install]
WantedBy=multi-user.targetA pod Quadlet unit carries networking decisions that belong at the pod layer:
Network=app.networkattaches the shared namespace to your user-defined network.PublishPort=18080:8080maps host port 18080 to container port 8080 inside the pod network namespace. The lab image isubi9/httpd-24, which listens on 8080 — not port 80.- Containers that join this pod should not publish the same ports independently.
[Service] Restart=always is intentional. The default generated pod service uses Restart=on-failure, which interacts badly with ExitPolicy=stop — I reproduce that trap in a dedicated section below.
Attach a .container to the pod with Pod=
The web member mounts the volume and joins the pod:
[Unit]
Description=Web container in app pod
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
ContainerName=quadlet-web
Pod=app.pod
Volume=app-data.volume:/var/lib/app
[Service]
Restart=on-failureSave as web.container. Pod=app.pod does more than append --pod to podman run. Quadlet also emits BindsTo=app-pod.service and After=app-pod.service on the generated web.service, and Requires=app-data-volume.service from the Volume= reference.
After you add or change any Quadlet file, reload systemd so the generator picks up new units:
sudo systemctl daemon-reloaddaemon-reload exits silently when it succeeds.
Add a second container and start the stack
A sidecar member shares the pod network without its own volume:
[Unit]
Description=Sidecar container in app pod
[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
ContainerName=quadlet-sidecar
Pod=app.pod
Exec=sleep 3600
[Service]
Restart=on-failureSave as sidecar.container, reload systemd again, then start the pod service. Starting app-pod.service pulls up the pod infra container and every member with default StartWithPod=true:
sudo systemctl daemon-reloadBring the application pod online:
sudo systemctl start app-pod.serviceConfirm the pod is running:
sudo podman pod ps --filter name=quadlet-appSample output:
NAME STATUS CREATED
quadlet-app Running 41 seconds agoList members and shared port mapping:
sudo podman ps --pod --filter pod=quadlet-appSample output:
NAMES PODNAME STATUS PORTS
quadlet-app-infra quadlet-app Up 45 seconds 0.0.0.0:18080->8080/tcp
quadlet-sidecar quadlet-app Up 44 seconds 0.0.0.0:18080->8080/tcp
quadlet-web quadlet-app Up 44 seconds 0.0.0.0:18080->8080/tcp, 8443/tcpEvery member shows the pod-published port because they share one network namespace. A host connection to the mapped port reaches the web listener:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/Sample output:
403HTTP 403 means the connection reached httpd inside the pod. An empty reply or connection refused would mean the port map or listener is wrong.
Understand StartWithPod=
When a .container file sets Pod=app.pod, Quadlet defaults StartWithPod=true. With that default:
- the container starts when the pod service starts
- the container stops when the pod stops
- the container restarts when the pod restarts
You normally omit the key because true is already implied.
StartWithPod=false
Set StartWithPod=false when a member should stay out of the automatic pod start but still belong to the pod when you start it yourself:
[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
ContainerName=quadlet-maint
Pod=app.pod
StartWithPod=false
Exec=sleep 3600Save as maint.container. After daemon-reload, only the web and sidecar members came up when I started app-pod.service — quadlet-maint was absent from podman ps. Starting its own service joined it to the running pod:
sudo systemctl start maint.serviceSample output from podman ps --filter name=quadlet-maint:
quadlet-maint Up 1 secondTypical uses:
- maintenance worker you trigger on demand
- debug companion during an incident
- optional sidecar that should not block pod start
The container is still stopped with the pod. If it was running before a pod restart, it is restarted with the pod; if it was already stopped, the pod restart does not start it.
The StartWithPod= dependency is usually better than hand-written ordering
If Pod=app.pod is already set, Quadlet understands the pod-to-container relationship. You do not need duplicate Requires=app-pod.service lines on every member — the generator emits BindsTo and After for you.
Reserve manual [Unit] dependencies for relationships Quadlet cannot infer — for example, an application container that must start after a separate database container unit even though they are not in the same pod.
Add application dependencies with After= and Requires=
When one Quadlet unit must wait for another, reference the source filename in [Unit]:
[Unit]
Requires=database.container
After=database.containerThe difference matters:
Requires=— start and availability coupling. Ifdatabase.containerfails to start, dependent units fail too.After=— ordering only. The dependent unit starts later but does not require the other unit to be active.
Current Quadlet translates dependencies that reference other Quadlet source names such as database.container or app.pod into the generated service names. Use those source-unit references rather than guessing runtime names like systemd-database.
Automatic dependencies from resource references
Quadlet infers systemd dependencies when a [Container] or [Pod] key references another Quadlet file by suffix:
Pod=app.pod
Volume=app-data.volume:/var/lib/app
Network=app.networkReferencing app-data.volume is better than hard-coding systemd-app-data in hand-written Requires= lines — the generator maps the source name to the correct service and keeps dependencies aligned when you rename files.
The generated web.service on the lab host shows the result:
systemctl cat web.serviceRelevant excerpt:
Requires=app-data-volume.service
After=app-data-volume.service
BindsTo=app-pod.service
After=app-pod.service
ExecStart=/usr/bin/podman run ... --pod quadlet-app ...Image=app.image would similarly pull in an .image unit when you build images declaratively. The pattern is the same: reference the Quadlet source file, not the runtime object name.
Pod port publishing
Host port maps belong on the pod when members share its network namespace:
[Pod]
PublishPort=18080:8080Traffic hits the shared namespace; any member with a listener on 8080 can answer. Do not repeat PublishPort= on .container files that set Pod=app.pod — you would be fighting the pod layer and duplicating maps. See Podman pod networking for localhost routing, infra containers, and DNS inside pods.
Pod-level user namespace
User namespace mode can be set once on the pod:
[Pod]
UserNS=keep-idContainers that join inherit the pod user namespace configuration. Do not set conflicting per-container UserNS= values on members after they join — pick pod-level or container-level deliberately. Detailed ID mapping belongs in Podman user namespaces.
Pod exit policy
Quadlet .pod units default to:
[Pod]
ExitPolicy=stopWhen the last regular container in the pod stops, the pod stops and the app-pod.service unit can exit. That is usually what you want for batch-style pods. The generated app-pod.service shows the flag on podman pod create:
systemctl cat app-pod.serviceRelevant excerpt:
ExecStartPre=/usr/bin/podman pod create ... --exit-policy stop ... --publish 18080:8080 --network quadlet-app --name quadlet-appExitPolicy=continue keeps the pod alive after application containers exit — useful when an infra holder or long-lived sidecar should outlast short jobs. General pod exit semantics are covered in Podman pods; here the focus is how that policy interacts with systemd restart behavior.
The silent pod restart trap
This is the production gotcha most Quadlet pod guides skip.
Quadlet generates pod services with:
[Service]
Restart=on-failureCombined with default ExitPolicy=stop, a normal application shutdown looks like success to systemd:
all app containers exit
↓
pod stops with exit code 0
↓
systemd records Result=success
↓
Restart=on-failure does not fire
↓
pod stays dead until manual interventionI reproduced it with a one-shot quay.io/podman/hello container in trap.pod:
[Unit]
Description=Restart trap demo pod
[Pod]
PodName=trap-pod
ExitPolicy=stopMember trap-web.container:
[Container]
Image=quay.io/podman/hello
ContainerName=trap-web
Pod=trap.podAfter daemon-reload, start the trap pod and inspect systemd state:
sudo systemctl start trap-pod.serviceWait a few seconds for hello to print and exit, then read the service properties:
systemctl show trap-pod.service -p ActiveState,SubState,Result,NRestarts,Restart --no-pagerSample output with defaults:
Restart=on-failure
Result=success
NRestarts=0
ActiveState=inactive
SubState=deadThe pod stopped cleanly and systemd did not restart it — exactly the silent failure mode for a workload that should have come back.
Fix with Restart=always
Quadlet supports overriding the generated restart policy directly in the .pod source:
[Service]
Restart=always
RestartSec=3Add that block to trap.pod, then reload and verify the effective value:
sudo systemctl daemon-reloadsystemctl show trap-pod.service -p Restart --no-pagerIf it still shows on-failure, troubleshoot the generated unit or stale drop-in configuration before adding another override. Check systemctl cat trap-pod.service and any files under /etc/systemd/system/trap-pod.service.d/.
On one lab run, the expected override did not appear until the generated configuration was rechecked; always verify the effective systemd property.
When Restart=always is in effect, start the trap pod again:
sudo systemctl reset-failed trap-pod.service
sudo systemctl start trap-pod.serviceAfter hello exits, systemd restarts the pod. With a one-shot image, restarts eventually hit the start limit:
systemctl show trap-pod.service -p ActiveState,NRestarts,Result --no-pagerSample output:
NRestarts=2
Result=start-limit-hit
ActiveState=failedNRestarts=2 proves systemd retried — unlike the default where NRestarts=0 and the unit stayed inactive (dead). For real long-running images, Restart=always on the pod service keeps the stack up without hitting the limit.
Set Restart=always on the .pod unit for always-on applications. Keep Restart=on-failure only when a clean permanent stop after success is what you intend.
Resource limits and CgroupsMode
Quadlet .container units default to CgroupsMode=split, which places conmon and the payload in separate cgroups — good for per-container systemd services. When several containers join one pod and you need pod-level resource limits, consider aligning cgroup mode across members:
CgroupsMode=enabled— traditional unified cgroup for the container payloadCgroupsMode=no-conmon— keeps conmon in the systemd unit cgroup
Match modes across pod members when applying pod-scoped limits. This is not a full cgroup v2 tutorial — tune based on whether systemd, the pod, or individual containers should own the hierarchy.
Inspect generated dependencies
systemctl list-dependencies makes automatic wiring visible:
systemctl list-dependencies app-pod.service --plainSample output:
app-pod.service
app-network.service
sidecar.service
web.serviceThe pod unit sits above the network and member container services. Compare with the generated pod unit ordering:
systemctl cat app-pod.serviceRelevant excerpt:
Requires=app-network.service
After=app-network.service
Wants=sidecar.service
Before=sidecar.service
Wants=web.service
Before=web.serviceWants=/Before= on members reflects StartWithPod=true — the pod service orchestrates its children.
Startup ordering vs application readiness
After=database.container only guarantees systemd starts units in order. It does not prove PostgreSQL accepts connections, migrations finished, or an HTTP health endpoint returns 200.
For readiness, use:
- container
HealthCmd=/Notify=directives from Podman Quadlet container file - application retry logic on first connect
Type=notifyservices when the app supports sdnotify
Do not paper over readiness with ExecStartPre=/bin/sleep 10 — that only adds latency and still races on slow boots.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Pod service inactive (dead) after containers exit |
ExitPolicy=stop plus Restart=on-failure on the generated pod service |
Set Restart=always on the .pod unit; verify with systemctl show -p Restart |
| Host port connection refused | PublishPort= on a .container instead of the .pod |
Move port maps to [Pod]; confirm listener port inside the image |
Container not running after app-pod.service start |
StartWithPod=false on that member |
Start the container service manually or set StartWithPod=true |
systemctl start web.service fails with pod errors |
app.pod missing, failed to generate, or pod creation itself failed |
Check systemctl status app-pod.service, journalctl -u app-pod.service, and verify Pod=app.pod references an existing Quadlet file |
| Generated unit missing volume dependency | Volume= uses a raw path instead of name.volume: syntax |
Reference app-data.volume:/mount so Quadlet can infer app-data-volume.service |
References
- podman-pod.unit(5) — Quadlet
.podunit directives - podman-container.unit(5) —
Pod=,StartWithPod=, and container keys - Podman pod create — runtime flags behind
[Pod] - systemd.unit(5) —
Requires=,After=, andRestart=semantics
Summary
Quadlet .pod files turn a Podman pod into a first-class systemd service — app.pod becomes app-pod.service, and members declare Pod=app.pod instead of repeating runtime pod names. Wiring app.network, app-data.volume, app.pod, web.container, and sidecar.container gives you a complete multi-container application where the pod owns the shared network, published ports, and exit policy while containers inherit automatic BindsTo and volume dependencies.
StartWithPod=true is the default and keeps members aligned with pod start, stop, and restart. Set StartWithPod=false for optional companions you will start manually. When you need ordering beyond what resource references infer, use Requires= and After= with other Quadlet source filenames — not guessed systemd-* runtime names.
The trap to remember is restart semantics: default ExitPolicy=stop lets a pod exit successfully when application containers finish, and Restart=on-failure on the generated pod service will not bring it back. Set Restart=always on always-on pod workloads and confirm the generated unit picked it up with systemctl show -p Restart. Port maps and UserNS= belong on the pod layer for joined containers. Check systemctl list-dependencies and systemctl cat when you want to see how Quadlet translated your declarations into real unit files.

