The .container unit¶
Scope: the course page's §4 covers the
.containerskeleton and the day-to-day[Container]keys table. This page slows down on why its sections behave differently, exactly what the generation step produces, how it names the result, and how to verify it — the mechanics behind "edit,daemon-reload, done," not just the fact of it.
TL;DR — in 30 seconds:
- Quadlet only interprets
[Container]—[Unit]/[Install]pass through untouched into the generated unit, and[Service]is merged with theExecStart=/ExecStop=lines Quadlet builds. - Only
.containermaps straight to<name>.service—.volume/.network/.podget a-volume/-network/-podsuffix inserted so a same-named container and volume never collide. quadlet -dryrunpreviews what a reload would generate;systemctl cat <unit>.serviceshows what's actually loaded right now — don't mix up the preview with the receipt.
1. Four sections, three different treatments¶
A .container file can carry four systemd-style sections, and Quadlet treats them differently:
[Unit]and[Install]are plain systemd syntax. Quadlet copies them into the generated.servicefile essentially untouched — the sameDescription=,After=,Wants=,WantedBy=you'd hand-write in any unit.[Container]is the only section Quadlet actually interprets. Every key in it —Image=,Exec=,Volume=,Network=,PublishPort=, and the rest of the course page's table — gets translated into fragments of the generated[Service]section: theExecStart=line that runspodman run, plus theExecStartPre=/ExecStop=housekeeping that pulls the image and tears the container down cleanly.[Service]is merged, not replaced. Whatever you write here (Restart=always,RestartSec=,TimeoutStartSec=) sits alongside theExecStart=/ExecStop=lines Quadlet generates from[Container]— your directives and Quadlet's coexist in the same final section.
That split — pass-through, translate, merge — is why editing [Unit]/[Install] feels exactly like
editing a normal unit, while [Container] has its own vocabulary you have to learn from the keys table.
2. What generation produces, and how it's named¶
On daemon-reload (and at boot), systemd's generator step runs Quadlet's generator binary, which reads
every source file in the search directories and synthesizes a .service unit for each one — loaded as if
it had been sitting on disk all along. Naming follows the source suffix, but not uniformly:
| Source suffix | Generated service name | Example |
|---|---|---|
.container |
<basename>.service (no suffix added) |
myapp.container → myapp.service |
.volume |
<basename>-volume.service |
pgdata.volume → pgdata-volume.service |
.network |
<basename>-network.service |
webapp.network → webapp-network.service |
.pod |
<basename>-pod.service |
webapp.pod → webapp-pod.service |
Only .container maps straight to .service with no inserted word — the others get a -volume/
-network/-pod marker so a same-named container and volume never collide. Worth internalising, or
you'll go looking for a generated unit under the wrong name.
3. Verifying the generated unit — two different questions¶
Two commands answer two different questions, and mixing them up is a common source of confusion:
quadlet -dryrun(course page §3) previews what Quadlet would generate from your source files, without writing or loading anything — "if I reload now, is this what I'll get?" Run it before you commit to adaemon-reloadyou're unsure about.systemctl cat <unit>.serviceshows what's actually loaded right now — the real generated unit, post-reload, with your[Service]lines merged into Quadlet's. This is the one that confirms reality after the fact, and it's the command the module's gate asks you to use to prove the generated unit matches your.containerfile's intent.
-dryrun is the preview; systemctl cat is the receipt.
4. Repeatable keys and the escape hatch¶
Several [Container] keys — Volume=, Network=, Environment=, PublishPort= — are repeatable: each
line you add contributes one more entry rather than overwriting the last, and for Volume= the order you
write them in becomes the mount order in the generated ExecStart=. PodmanArgs= is the escape hatch: it
appends its value verbatim to the end of the constructed podman run command, bypassing everything else
Quadlet validates. Reach for a named directive first — reserve PodmanArgs= for when nothing in the table
covers what you need.
flowchart TB
subgraph Source["myapp.container (source — you write this)"]
U["[Unit]"]
C["[Container]"]
S["[Service]"]
I["[Install]"]
end
subgraph Gen["myapp.service (generated — never edit)"]
U2["[Unit] — copied through"]
S2["[Service] — your lines + Quadlet's ExecStart / ExecStartPre / ExecStop"]
I2["[Install] — copied through"]
end
U --> U2
C -->|translated| S2
S -->|merged| S2
I --> I2
S2 -->|systemctl start| Run["podman container<br/>(systemd-myapp)"]
5. How this connects¶
- This course's next page,
02-pods-networks-volumes.md— the-volume/-network/-podsuffix rule from §2 above is where it actually matters: that page covers the units this naming pattern applies to, plus the one-shot "exited means healthy" service state a.containernever shows. - systemd & journald (module 09, Linux (RHEL), §3 "Editing units safely") —
the same discipline as this page's hand-editing gotcha below, one level up: that page's drop-ins exist so
a package update can't silently overwrite your change to a vendor unit, the same way
daemon-reloadsilently overwrites hand-edits to a Quadlet-generated one. - Podman (module 03, Containers course page, §6–§7 building and
running containers) — the
[Container]keys §1 says get "translated" are translating straight into thepodman run/build vocabulary that page covers directly;PodmanArgs=(§4) is the literal escape hatch back into any Podman flag with no dedicated key.
Common gotchas
- Hand-editing the generated
.serviceunit instead of the source.containerfile. Fix: edit the source anddaemon-reload— the generated unit is rebuilt from source on every reload, so edits made directly to it don't survive the next one. - Assuming a repeatable key like
Volume=overwrites the previous line. Fix: repeatable keys are additive — each line adds one more entry (and forVolume=, sets the mount order); to remove one, delete its line rather than expecting a later line to replace it. - Reaching for
PodmanArgs=before checking the keys table. Fix:PodmanArgs=bypasses Quadlet's validation entirely — use it only for the rare option with no dedicated key, not as a shortcut around learning the table. - Confusing
-dryrun's preview with what's actually loaded. Fix:-dryrunonly shows what would be generated; runsystemctl cat <unit>.serviceafter a realdaemon-reloadto confirm what's actually running.
Check yourself — you change a key in myapp.container and run systemctl restart myapp.service, but the container comes up with the old configuration. What step is missing?
systemctl daemon-reload — a plain restart just reruns the existing generated unit. The generator only
re-reads the source .container file and rebuilds the service on daemon-reload (or at boot).
Check yourself — you have pgdata.volume and myapp.container. What service name does the volume generate, and why does it differ from the container's naming pattern?
pgdata-volume.service — only .container maps straight to <name>.service; .volume/.network/
.pod get a suffix inserted so a same-named container and volume never produce colliding unit names.
You can defend this when you can name, for any key in a .container file, which of the three
treatments — pass-through, translate, merge — applies to its section; explain why myapp.container becomes
myapp.service while pgdata.volume becomes pgdata-volume.service; and choose correctly between
-dryrun and systemctl cat depending on whether you're previewing a change or confirming what's already
loaded.