Podman Quadlet `.container` File Explained with Examples

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:

ini
[Container]
Image=quay.io/podman/hello

Save it as hello.container under /etc/containers/systemd/, reload systemd, and start the generated service:

bash
sudo systemctl daemon-reload

Start the generated unit:

bash
sudo systemctl start hello.service

The 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:

ini
[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.target

Companion Quadlet files on the lab host:

web.network:

ini
[Network]
NetworkName=web-net

web-data.volume:

ini
[Volume]
VolumeName=web-data

Environment file at /etc/web/web.env:

text
APP_ENV=production
LOG_LEVEL=info

The 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]:

ini
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24:1.0

Image 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.image adds a generator dependency on web-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>:

text
hello.container  →  hello.service  →  systemd-hello container

Set an explicit name when scripts or operators expect a fixed container identity:

ini
[Container]
ContainerName=web

Result:

text
web.container  →  web.service  →  web container

Do 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:

ini
[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
Exec=sleep 3600

That becomes the equivalent of:

bash
podman run registry.access.redhat.com/ubi9/ubi-minimal sleep 3600

Exec= 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=:

ini
Environment=APP_ENV=production LOG_LEVEL=info

Load variables from a file with EnvironmentFile=:

ini
EnvironmentFile=/etc/web/web.env
  • Environment= sets variables directly in the unit
  • EnvironmentFile= 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:

bash
sudo podman exec web printenv APP_ENV LOG_LEVEL

Sample output:

output
production
info

Both 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:

ini
EnvironmentFile=./web.env

Place 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:

ini
Volume=web-data.volume:/var/lib/web

Bind-mount a host directory:

ini
Volume=/srv/web/config:/etc/web-config:Z,ro

When 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:

text
Requires=web-data-volume.service
After=web-data-volume.service

SELinux :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:

ini
Mount=type=bind,src=/srv/web,dst=/data,ro

Quadlet 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:

ini
Network=web.network

Or attach to an existing Podman network by name:

ini
Network=app-network

Network modes work the same as podman run --network:

ini
Network=host
ini
Network=none

A .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:

ini
PublishPort=18080:8080

Multiple mappings:

ini
PublishPort=8080:80
PublishPort=8443:443

Bind to a host IP:

ini
PublishPort=127.0.0.1:8080:80

This 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:

bash
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18080/

Sample output:

output
403

HTTP 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:

ini
User=10001

Add a group:

ini
User=10001
Group=10001

These 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:

ini
[Service]
User=podtest

or:

ini
DynamicUser=yes

expecting 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 .container file 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=:

ini
UserNS=keep-id

Or pin mapped IDs:

ini
UserNS=keep-id:uid=1000,gid=1000

UserNS= 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:

ini
HealthCmd=/bin/sh -c "curl -s -o /dev/null http://127.0.0.1:8080/"
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3

Use 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:

bash
sudo podman healthcheck run web

Sample output:

output
healthy

Inspect persisted health state:

bash
sudo podman inspect web --format '{{.State.Health.Status}}'

Sample output:

output
healthy

A 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:

ini
Notify=true

Wait for Podman's health state to become healthy:

ini
Notify=healthy

The 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]:

ini
Memory=512M
PidsLimit=512

Quadlet 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:

ini
CgroupsMode=split

split places conmon and the container payload in separate cgroups, which suits systemd service management and cgroup delegation.

Inspect the generated command:

bash
sudo systemctl show -p ExecStart web.service

Sample output (trimmed):

output
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:

ini
[Service]
Restart=always
RestartSec=5
TimeoutStartSec=300

These 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:

ini
PodmanArgs=--some-new-option=value

PodmanArgs= appends to the generated command. Prefer native directives whenever they exist:

  • the generator can infer dependencies for Volume=, Network=, and Image=
  • 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:

bash
sudo systemctl cat web.service

Sample output (trimmed):

output
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:

bash
sudo systemctl show -p ExecStart web.service

This 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


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

1. 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.

2. 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.

3. 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.

4. 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.

5. 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.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)