| 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 4.6+ and systemd |
| Privilege | sudo for rootful examples on the lab host |
| Scope | Task-oriented [Container] directive reference — image, naming, exec, environment, paths, volumes, mounts, networks, ports, user identity, user namespaces, health checks, Notify, CgroupsMode, resource limits, PodmanArgs, and where [Service] directives belong. Does not cover every podman-container.unit(5) key, rootless setup, .pod orchestration, volume or network creation depth, auto-update, or full troubleshooting. |
You already know Quadlet turns .container files into systemd services from Podman Quadlet with systemd. This guide focuses on the [Container] section — the directives you reach for when wiring image, storage, network, ports, identity, health, and readiness into a production unit.
The examples use rootful units under /etc/containers/systemd/. The same [Container] keys apply to rootless Quadlet files; only paths and systemctl --user differ.
Minimal .container file
A working image-based unit needs only an image reference:
[Container]
Image=quay.io/podman/helloSave it as hello.container under /etc/containers/systemd/, reload systemd, and start the generated service:
sudo systemctl daemon-reloadStart the generated unit:
sudo systemctl start hello.serviceThe hello image prints once and exits, so hello.service may show inactive (dead) shortly after start. That is normal for a one-shot smoke test. A long-running workload needs a process that stays up — the web example below uses httpd-24 for that reason.
For a normal image-based .container unit, Image= is the only required [Container] key. Rootfs= is an alternative for rootfs-based containers and cannot be combined with Image=.
Complete practical .container example
This lab unit wires network, volume, environment, port publishing, and health readiness together:
[Unit]
Description=Web application
After=network-online.target
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
ContainerName=web
Environment=APP_ENV=production
EnvironmentFile=/etc/web/web.env
Network=web.network
Volume=web-data.volume:/var/lib/web
Volume=/srv/web/config:/etc/web-config:Z,ro
PublishPort=18080:8080
User=0
HealthCmd=/bin/sh -c "curl -s -o /dev/null http://127.0.0.1:8080/"
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3
Notify=healthy
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=multi-user.targetCompanion Quadlet files on the lab host:
web.network:
[Network]
NetworkName=web-netweb-data.volume:
[Volume]
VolumeName=web-dataEnvironment file at /etc/web/web.env:
APP_ENV=production
LOG_LEVEL=infoThe sections below walk through each directive using this unit. Network and volume creation depth lives in Podman Quadlet volume and network units when those articles ship; here we focus on how a .container references them.
Choose the image with Image=
Pin the container image in [Container]:
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24:1.0Image selection tips:
- Fully qualified registry names are preferable for reproducibility
- Tags are mutable — the same tag can point at different digests later
- Digests are immutable when you need a fixed root filesystem
- Another Quadlet unit can be referenced:
Image=web.imageadds a generator dependency onweb-image.service
Image pull policy and .image unit behavior are out of scope here.
Set the container name
Without ContainerName=, Quadlet names the Podman container systemd-<unit-basename>:
hello.container → hello.service → systemd-hello containerSet an explicit name when scripts or operators expect a fixed container identity:
[Container]
ContainerName=webResult:
web.container → web.service → web containerDo not confuse ContainerName= with ServiceName=. On Podman 5.8.2, web.container generates web.service. Newer Quadlet versions also provide ServiceName= to override the generated service name, so check your installed version before relying on filename-only naming.
Override the image command with Exec=
Exec= replaces the image default command, similar to arguments after the image on podman run:
[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
Exec=sleep 3600That becomes the equivalent of:
podman run registry.access.redhat.com/ubi9/ubi-minimal sleep 3600Exec= is the container process command. It is not systemd ExecStart= — the generator owns ExecStart and embeds the full podman run line there.
Set environment variables
Inline variables use Environment=:
Environment=APP_ENV=production LOG_LEVEL=infoLoad variables from a file with EnvironmentFile=:
EnvironmentFile=/etc/web/web.envEnvironment=sets variables directly in the unitEnvironmentFile=loads key/value pairs from a host file- The file must exist before the service starts
After web.service is running, confirm the variables inside the container:
sudo podman exec web printenv APP_ENV LOG_LEVELSample output:
production
infoBoth values appear — APP_ENV from Environment= and LOG_LEVEL from EnvironmentFile=.
Relative files and Quadlet paths
When a referenced path begins with ., Quadlet resolves it relative to the Quadlet file location:
EnvironmentFile=./web.envPlace web.env beside web.container in /etc/containers/systemd/ (or your chosen Quadlet directory).
Paths beginning with % are treated as systemd specifiers, not literal filenames. Use ./ when you intend a relative file next to the unit. Bind-mount sources that start with . follow the same resolution rule.
Mount volumes
Reference a Quadlet volume unit — the generator adds a systemd dependency automatically:
Volume=web-data.volume:/var/lib/webBind-mount a host directory:
Volume=/srv/web/config:/etc/web-config:Z,roWhen the source ends in .volume, Quadlet resolves the corresponding volume unit and wires web-data-volume.service ahead of web.service. On the lab host, the generated unit shows:
Requires=web-data-volume.service
After=web-data-volume.serviceSELinux :z / :Z labels, ownership, and backup workflows belong in Podman volume permissions and storage guides — not duplicated here.
Use Mount= for structured mount syntax
Mount= maps to Podman --mount when you need explicit key/value mount options:
Mount=type=bind,src=/srv/web,dst=/data,roQuadlet also accepts .volume and .image references in Mount= where your Podman version supports them. Prefer Volume= for the common named-volume and bind-mount cases; use Mount= when --mount syntax is clearer. The Podman volume vs bind mount guide covers CLI -v versus --mount trade-offs.
Attach the container to a network
Reference a Quadlet network unit:
Network=web.networkOr attach to an existing Podman network by name:
Network=app-networkNetwork modes work the same as podman run --network:
Network=hostNetwork=noneA .network reference creates an implicit dependency. The lab web.service unit includes Requires=web-network.service and After=web-network.service without manual [Unit] lines. Network creation and DNS belong in Podman networking modes.
Publish ports
Map host ports to container ports:
PublishPort=18080:8080Multiple mappings:
PublishPort=8080:80
PublishPort=8443:443Bind to a host IP:
PublishPort=127.0.0.1:8080:80This maps to podman run -p. The lab unit publishes host 18080 to container 8080 because UBI httpd-24 listens on 8080 inside the container.
Verify from the host:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18080/Sample output:
403HTTP 403 confirms the port mapping works — httpd is responding. Full -p syntax and pod-level publishing rules live in Podman port mapping and Podman pod networking.
Run as a container user
Set the UID inside the container:
User=10001Add a group:
User=10001
Group=10001These control the identity inside the container namespace, not the Linux account that runs the systemd service on the host.
Do not use [Service] User= to make a rootful Quadlet rootless
This trap deserves its own warning. Do not write:
[Service]
User=podtestor:
DynamicUser=yesexpecting a rootful Quadlet under /etc/containers/systemd/ to run as an unprivileged host user. Quadlet does not support User=, Group=, or DynamicUser= in [Service] for that purpose.
For rootless workloads:
- Sign in as the target Linux account
- Place the
.containerfile in a rootless Quadlet search path such as~/.config/containers/systemd/ - Manage the unit with
systemctl --user
See Rootless Podman Quadlet for paths, lingering, and boot behavior.
Configure user namespaces
Map container user IDs with UserNS=:
UserNS=keep-idOr pin mapped IDs:
UserNS=keep-id:uid=1000,gid=1000UserNS= controls how host and container user IDs relate. Full mode semantics and volume permission interactions belong in Podman user namespaces.
Health checks
Health directives run inside the container:
HealthCmd=/bin/sh -c "curl -s -o /dev/null http://127.0.0.1:8080/"
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3Use the container port (8080 here), not the published host port (18080). Avoid curl -f when the application returns a non-2xx code on success — UBI httpd returns 403 on the default page, which makes curl -f fail even when the server is up.
Run the check manually:
sudo podman healthcheck run webSample output:
healthyInspect persisted health state:
sudo podman inspect web --format '{{.State.Health.Status}}'Sample output:
healthyA health check proves the process responds inside the container. Service readiness is a separate concern — see Notify= next.
Notify= and systemd readiness
Quadlet .container services normally use systemd Type=notify. Podman and conmon can signal startup through sdnotify integration.
Wait for the container application to send READY=1:
Notify=trueWait for Podman's health state to become healthy:
Notify=healthyThe lab unit uses Notify=healthy. The generated podman run line includes --sdnotify=healthy, and web.service stays in activating until the health check passes.
| Value | Readiness signal |
|---|---|
| (default conmon integration) | conmon notifies when the container process starts |
Notify=true |
the application inside the container sends sdnotify READY=1 |
Notify=healthy |
Podman health state must be healthy before the unit is active |
Notify=healthy matters when downstream units or auto-update rollback should wait for a real health signal, not merely a running PID.
Resource limits
Set common container limits in [Container]:
Memory=512M
PidsLimit=512Quadlet can combine Podman container limits with normal systemd cgroup controls in [Service]. CPU throttling may use supported Podman keys on your version or systemd CPUQuota= / MemoryMax= in [Service] when that fits your deployment.
Do not duplicate the full cgroup tuning article here — apply limits that match your workload and verify with systemd-cgtop or podman stats.
Quadlet CgroupsMode=split default
The normal podman run CLI default is enabled. Quadlet .container units default to split:
CgroupsMode=splitsplit places conmon and the container payload in separate cgroups, which suits systemd service management and cgroup delegation.
Inspect the generated command:
sudo systemctl show -p ExecStart web.serviceSample output (trimmed):
ExecStart={ path=/usr/bin/podman ; argv[]=/usr/bin/podman run --name web --replace --rm --cgroups=split --network web-net --sdnotify=healthy ...--cgroups=split appears even when you omit CgroupsMode= in the source file.
For containers inside a Quadlet .pod where pod-level cgroup limits must apply, consider enabled or no-conmon instead. Do not override the default without a reason.
Restart and systemd options belong in [Service]
Container restart policy belongs in systemd, not in podman run --restart:
[Service]
Restart=always
RestartSec=5
TimeoutStartSec=300These are ordinary systemd directives passed through by the generator. Long image pulls may need a higher TimeoutStartSec on first start.
Do not force every systemd option into [Container]. Keep Podman-specific settings under [Container] and service behavior under [Service].
Use PodmanArgs= only as an escape hatch
When Quadlet has no dedicated key yet, append raw podman run flags:
PodmanArgs=--some-new-option=valuePodmanArgs= appends to the generated command. Prefer native directives whenever they exist:
- the generator can infer dependencies for
Volume=,Network=, andImage= - arbitrary flags may conflict with dedicated keys
- units become harder to read and review
Reach for PodmanArgs= only for new Podman flags that lack a Quadlet equivalent on your version.
Inspect the generated podman run command
Read the full generated unit:
sudo systemctl cat web.serviceSample output (trimmed):
Requires=web-network.service
After=web-network.service
Requires=web-data-volume.service
After=web-data-volume.service
[X-Container]
Image=registry.access.redhat.com/ubi9/httpd-24
ContainerName=web
PublishPort=18080:8080
...
ExecStart=/usr/bin/podman run --name web --replace --rm --cgroups=split ...Or print only ExecStart:
sudo systemctl show -p ExecStart web.serviceThis is the fastest way to see how [Container] directives translate on your Podman version. Generator failures and missing units belong in Podman Quadlet troubleshooting.
Common .container configuration errors
| Symptom | Likely cause | Fix |
|---|---|---|
Service stuck in activating with Notify=healthy |
Health command fails or uses wrong port | Fix HealthCmd; use container port, not host port |
curl -f health check always fails |
Application returns non-2xx on success | Drop -f or accept the real success code |
Container named systemd-web unexpectedly |
No ContainerName= set |
Add ContainerName= or expect the systemd- prefix |
| Network or volume not ready | Referenced .network / .volume missing or misnamed |
Match Quadlet filenames; check generator Requires= lines |
[Service] User= ignored for rootless behavior |
Rootful path with systemd user drop-in | Move unit to rootless Quadlet paths |
References
- Podman: Container Quadlet reference
- Podman: systemd units using Quadlet
- Podman: Network Quadlet reference
- Podman: Volume Quadlet reference
- RHEL 10: Porting containers to systemd using Podman
Summary
A .container Quadlet file is a task layer on top of podman run, not a copy of the full podman-container.unit(5) manual. For an image-based .container unit, Image= is the only required [Container] key; Rootfs= is the alternative for rootfs-based containers. ContainerName= controls the Podman container identity separately from web.service. Environment, volumes, networks, and ports use dedicated directives — and references ending in .volume or .network create systemd dependencies you do not need to wire by hand.
Keep restart policy, timeouts, and boot targets in [Service] and [Install]. Do not use [Service] User= to fake rootless behavior on a rootful unit. Quadlet defaults to CgroupsMode=split, which differs from the CLI — check systemctl show -p ExecStart when cgroup layout matters. Use Notify=healthy when systemd should wait for a real health signal, and reserve PodmanArgs= for flags that have no first-class Quadlet key yet.
If you are new to the generator model, start with Podman Quadlet with systemd. For rootless paths and lingering, continue with Rootless Podman Quadlet.
Frequently Asked Questions
What is the only required key in a Podman Quadlet container file?
For a normal image-based .container unit, Image= is the only required [Container] key. Rootfs= is an alternative for rootfs-based containers and cannot be combined with Image=. Every other directive is optional and maps to podman run flags or systemd behavior in the generated service.
What is the difference between ContainerName and the generated service name?
On Podman 5.8.2, web.container generates web.service. Newer Quadlet versions also provide ServiceName= to override the generated service name, so check your installed version before relying on filename-only naming. The Podman container defaults to systemd-web unless you set ContainerName=web. The service name and container name are separate identifiers.
Can I use User= in the [Service] section to make a rootful Quadlet rootless?
No. User=, Group=, and DynamicUser= in [Service] do not turn a rootful Quadlet into a rootless workload. Place the unit in a rootless Quadlet path and manage it with systemctl --user under that Linux account.
Why does my Quadlet container use cgroups split when the CLI default is enabled?
Quadlet defaults CgroupsMode to split, which places conmon and the container payload in separate cgroups suited for systemd service management. The normal podman run CLI default is enabled unless you override it.
When should I use PodmanArgs in a container Quadlet?
Use PodmanArgs only when Quadlet has no dedicated directive for a Podman flag you need. Prefer native keys such as PublishPort, Volume, and Environment because the generator can infer dependencies and produce readable units.

