Skip to content

Pods, networks & volumes

Scope: the course page's §5–§7 cover the .volume, .network, and .pod skeletons 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/.network units are one-shot "ensure it exists" services — a healthy one reports exited, not running. That's success, not a failure.
  • Reference a .volume/.network/.pod from a .container key and Quadlet auto-wires the ordering — but container-to-container application dependencies (API needs Postgres up) still need hand-written After=/Requires=.
  • A .pod shares 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= and PublishPort= from their own [Container] section — those belong on the .pod file instead. Every container in the pod shares one network identity, which is also why they can reach each other over localhost rather 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/-pod suffix naming and the auto-generated-on-daemon-reload mechanics that page covers are exactly why the units this page describes are named pgdata-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 .pod is 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/.network unit reporting anything other than "running" is broken. Fix: these are one-shot services — exited after a successful run is the expected, healthy state; read the result, not just the activity word.
  • Expecting Quadlet to auto-wire ordering between two .container units. 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-written After=/Requires= in [Unit].
  • Leaving Network=/PublishPort= on a member container inside a .pod. Fix: those keys move to the .pod file 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 no Network= key joins none of your named networks, so it can't be discovered by name — both sides need the same Network=<name>.network reference.
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.