Pods, networks & volumes¶
Scope: the course page's §5–§7 cover the
.volume,.network, and.podskeletons and when to reach for each. This page slows down on the mechanics behind them — why they run and report differently from a.container, how Quadlet wires the dependency graph between them for you, and what a pod actually shares between its members.
TL;DR — in 30 seconds:
.volume/.networkunits are one-shot "ensure it exists" services — a healthy one reports exited, not running. That's success, not a failure.- Reference a
.volume/.network/.podfrom a.containerkey and Quadlet auto-wires the ordering — but container-to-container application dependencies (API needs Postgres up) still need hand-writtenAfter=/Requires=. - A
.podshares its members' network + IPC namespace, but not storage — each container in a pod still declares its own volumes.
1. .volume and .network are "ensure it exists," not "keep it running"¶
A .container unit maps to a long-running process: the generated service stays active (running) for as
long as the container's main process is alive. .volume and .network units are a different shape —
one-shot services whose entire job is to create the underlying Podman object if it doesn't already
exist, then finish. Once that one-shot run succeeds, the unit reports as exited, not running — there
is no ongoing process behind a volume or a network, just a Podman object that now exists on disk or in the
network stack.
This trips people coming from .container's always-running model: seeing pgdata-volume.service as
"exited" looks like a failure until you internalise that exited-after-success is exactly what a correctly
working .volume/.network unit looks like. A crash-looped .container and a healthy .volume can both
show non-"running" states — read the result, not just the activity word, to tell them apart.
2. The dependency graph Quadlet writes for you — and the part it can't¶
Reference a .volume, .network, or .pod file from a .container's Volume=, Network=, or Pod=
key, and Quadlet inserts Requires= and After= between the generated services automatically. You never
hand-write Requires=pgdata-volume.service — Quadlet sees the Volume=pgdata.volume:... reference at
generation time and wires the ordering itself — true for every resource-reference key.
What Quadlet cannot infer: ordering between two .container units that depend on each other for
application reasons — an API container that needs Postgres actually accepting connections, not just the
volume existing. That dependency has no dedicated Quadlet key, so you write it the same way you would in a
hand-authored unit: After=postgres.service and Requires=postgres.service in the API container's
[Unit] section. Resource references (volume, network, pod) are auto-wired; container-to-container
ordering is still your job.
| Reference | Who injects the ordering |
|---|---|
Volume=<name>.volume:... in [Container] |
Quadlet, automatically |
Network=<name>.network in [Container] |
Quadlet, automatically |
Pod=<name>.pod in [Container] |
Quadlet, automatically |
After=/Requires= on another .container's service |
You, by hand in [Unit] |
flowchart LR
subgraph Auto["Auto-wired by Quadlet — resource references"]
Vol[".volume"] -->|"Requires=/After=<br/>from Volume="| Api["api.container"]
Net[".network"] -->|"Requires=/After=<br/>from Network="| Api
Pod[".pod"] -->|"Requires=/After=<br/>from Pod="| Api
end
subgraph Manual["You write by hand — application readiness"]
PG["postgres.container"] -->|"After=/Requires=<br/>hand-written"| Api2["api.container"]
end
3. What a pod actually shares between its members¶
A .pod groups containers into the same network and IPC namespaces — the same grouping primitive
Kubernetes calls a Pod, run locally by Podman. Two things follow directly from that:
- Network and ports move to the pod. Member containers drop
Network=andPublishPort=from their own[Container]section — those belong on the.podfile instead. Every container in the pod shares one network identity, which is also why they can reach each other overlocalhostrather than a container-name DNS lookup. - Everything else stays per-container. Filesystem, mounts, and
Volume=entries are not shared by pod membership — each container still declares its own volumes. Grouping containers in a pod is a networking/lifecycle decision, not a storage one.
Lifecycle is coupled at the pod level too: systemctl stop webapp-pod.service stops every member
container together, and systemctl start brings the pod's infrastructure up first, then starts members in
their own dependency order.
4. Network isolation and the DNS gotcha¶
Each .network file is a real, isolated Podman network — containers attached to it resolve each other by
container name through Podman's embedded DNS. The gotcha, if you're used to one shared default network for
everything: a container with no Network= key does not join any of your named networks, so it can't
discover or be discovered by name from containers that do declare one. If two containers need to talk by
name, both need the same Network=<name>.network reference — there's no implicit shared network to fall
back on.
Internal=true on a .network tightens this further: containers on an internal network can still reach
each other, but lose the route out to the host's external network entirely — useful for a database that
should never make an outbound connection.
flowchart TB
subgraph Pod["webapp.pod — shared network + IPC namespace"]
Nginx["nginx.container"]
Api["api.container"]
end
Net["webapp.network<br/>(isolated, embedded DNS)"] -->|"Network="| Pod
Vol["pgdata.volume<br/>(one-shot: ensure-exists)"] -->|"Volume="| PG["postgres.container<br/>(own network, own volume)"]
Nginx -.->|"localhost — shared ns"| Api
PG -.->|"postgres — DNS via webapp.network"| Api
5. How this connects¶
01-container-unit.md§2 (reciprocal) — the-volume/-network/-podsuffix naming and the auto-generated-on-daemon-reloadmechanics that page covers are exactly why the units this page describes are namedpgdata-volume.service,webapp-network.service,webapp-pod.service.- This course's next page,
03-autoupdate-and-rollout.md§3 — "pod membership doesn't change the unit boundary" builds directly on §3 above: a container inside a.podis still checked and restarted as its own service, so an auto-update cycle can restart one pod member without tearing down the pod. - Kubernetes Workloads (module 04, Kubernetes & Helm,
01-workloads.md) — the same grouping primitive, with one real difference: a Kubernetes Pod shares a network namespace and storage volumes between its containers, while a Podman.pod(§3 above) shares network + IPC but leaves storage per-container.
Common gotchas
- Assuming a
.volume/.networkunit reporting anything other than "running" is broken. Fix: these are one-shot services —exitedafter a successful run is the expected, healthy state; read the result, not just the activity word. - Expecting Quadlet to auto-wire ordering between two
.containerunits. Fix: Quadlet only auto-wires dependency edges for resource references (Volume=,Network=,Pod=); container-to- container ordering (e.g. API needs Postgres) still needs hand-writtenAfter=/Requires=in[Unit]. - Leaving
Network=/PublishPort=on a member container inside a.pod. Fix: those keys move to the.podfile itself — the pod owns the shared network identity, not its member containers. - Assuming two containers can reach each other by name with no
Network=key set. Fix: a container with noNetwork=key joins none of your named networks, so it can't be discovered by name — both sides need the sameNetwork=<name>.networkreference.
Check yourself — pgdata-volume.service shows as exited in systemctl status. Is something wrong?
No — .volume/.network units are one-shot: their job is to ensure the Podman object exists, then
they exit. exited after a successful run is the normal, healthy state; there's no ongoing process
behind a volume or network.
Check yourself — your API container needs Postgres actually accepting connections before it starts, but Quadlet isn't ordering them for you. Why not, and what do you write instead?
Quadlet only auto-wires ordering for resource references (Volume=, Network=, Pod=) — it can't
infer application-level readiness between two .container units. You add After=postgres.service and
Requires=postgres.service by hand in the API container's [Unit] section.
You can defend this when you can explain why a healthy .volume/.network unit shows exited rather
than running; identify which dependency edges Quadlet injects for you versus which you must write by hand;
state what a pod shares between its members and what it doesn't; and explain why a container with no
Network= key can't be reached by name from one that has it.