Skip to content

The .container unit

Scope: the course page's §4 covers the .container skeleton 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 the ExecStart=/ExecStop= lines Quadlet builds.
  • Only .container maps straight to <name>.service.volume/.network/.pod get a -volume/-network/-pod suffix inserted so a same-named container and volume never collide.
  • quadlet -dryrun previews what a reload would generate; systemctl cat <unit>.service shows 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 .service file essentially untouched — the same Description=, 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: the ExecStart= line that runs podman run, plus the ExecStartPre=/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 the ExecStart=/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.containermyapp.service
.volume <basename>-volume.service pgdata.volumepgdata-volume.service
.network <basename>-network.service webapp.networkwebapp-network.service
.pod <basename>-pod.service webapp.podwebapp-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 a daemon-reload you're unsure about.
  • systemctl cat <unit>.service shows 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 .container file'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/-pod suffix 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 .container never 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-reload silently 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 the podman 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 .service unit instead of the source .container file. Fix: edit the source and daemon-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 for Volume=, 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: -dryrun only shows what would be generated; run systemctl cat <unit>.service after a real daemon-reload to 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.