| 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 5.x and systemd |
| Privilege | sudo for rootful examples on the lab host |
| Scope | Quadlet .volume and .network supporting-resource units — naming, [Volume] and [Network] directives, referencing from .container files, automatic dependencies, User/Group ownership, drivers, Copy, NetworkDeleteOnStop, and network update limits. Assumes Quadlet basics. Does not cover general volume or network tutorials, macvlan/ipvlan depth, volume permission troubleshooting, or the full .container directive reference. |
| Related guides | Podman Quadlet container file |
Quadlet treats volumes and networks as supporting resources — oneshot systemd services that run podman volume create or podman network create before your containers start. A .container file references them by Quadlet filename (data.volume, app.network), and the generator wires Requires= and After= for you. This guide covers both unit types together because they share the same dependency model.
I tested every command on Podman 5.8.2. Confirm your build before copying examples:
podman --versionSample output:
podman version 5.8.2How supporting Quadlet resources work
A volume Quadlet file creates a oneshot service that ensures a Podman volume exists:
data.volume
│
▼
data-volume.service
│
▼
Podman volume
│
▼
web.container (Volume=data.volume:/mount)A network file follows the same pattern:
app.network
│
▼
app-network.service
│
▼
Podman network
│
▼
web.container (Network=app.network)When web.container lists Volume=data.volume:… or Network=app.network, Quadlet resolves the source filename to the generated service name and adds ordering dependencies. You normally do not hand-write Requires=data-volume.service unless you need extra relationships beyond what the reference infers.
Create a minimal .volume unit
The smallest useful volume unit sets an explicit runtime name:
[Volume]
VolumeName=application-dataSave as data.volume under /etc/containers/systemd/, reload systemd, and start the generated service:
sudo systemctl daemon-reloadCreate the volume with the generated oneshot service:
sudo systemctl start data-volume.serviceThe service should report active (exited) — oneshot units exit after podman volume create succeeds.
Confirm the volume exists:
sudo podman volume inspect application-data --format 'Name={{.Name}}'Sample output:
Name=application-dataNaming defaults without VolumeName=:
data.volume → data-volume.service → Podman volume systemd-dataVolumeName= overrides only the Podman object name, not the systemd service name derived from the filename.
Reference .volume from .container
Point the mount at the Quadlet source file, not the runtime volume name:
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
Volume=data.volume:/var/lib/appQuadlet translates data.volume into -v application-data:/var/lib/app on podman run and emits Requires=data-volume.service plus After=data-volume.service on the generated container unit. The filename is the stable contract between units.
Set volume ownership with User= and Group=
Host ownership on the volume mount uses [Volume] keys:
[Volume]
VolumeName=application-data
User=1000
Group=1000Reload and recreate if the volume already existed without ownership options:
sudo systemctl daemon-reloadStop the volume service, remove the existing volume, and start again so creation-time options apply — only when no container is using it and you can discard or have backed up the data:
sudo systemctl stop data-volume.service
sudo podman volume rm application-data
sudo systemctl start data-volume.serviceCreation-time volume settings apply when the volume is created. Changing the .volume file does not safely mutate an existing volume; recreate the volume when you intentionally want new creation parameters. Recreating a volume destroys its stored data.
Inspect what Podman stored:
sudo podman volume inspect application-data --format 'UID={{.UID}} GID={{.GID}}'Sample output:
UID=1000 GID=1000The generated data-volume.service passes ownership into volume creation:
systemctl cat data-volume.serviceRelevant excerpt:
ExecStart=/usr/bin/podman volume create --ignore --opt o=uid=1000,gid=1000 application-dataVolume ownership keys vs container User= / Group=
These keys live in different sections and mean different things:
[Volume] UID=/GID=— mount-point directory ownership viapodman volume create --uid/--gid[Volume] User=/Group=— host-side ownership passed as filesystem mount options[Container] User=/Group=— UID/GID the process runs as inside the container namespace
Setting User=1000 on the volume does not make the container process run as UID 1000. Setting User=0 on the container does not change volume host permissions. Configure each where the problem actually lives.
Mapping to podman volume create
Quadlet exposes two pairs of ownership keys with different semantics:
UID=/GID=— set the owner of the volume mount-point directory; map topodman volume create --uidand--gidUser=/Group=— passed as filesystem mount options on the volume mount
Example mappings:
UID=1234 → --uid 1234
GID=5678 → --gid 5678
User=123 → mount option o=uid=123
Group=192 → mount option o=group=192The lab example above uses User=1000 and Group=1000, which the generator passes as mount options — as shown in the ExecStart excerpt. Choose the pair that matches the ownership behavior you need. Options= for driver-specific settings is separate; verify keys against your installed podman-volume.unit(5).
Use Driver=
Most persistent data volumes stay on the default local driver:
[Volume]
VolumeName=web-data
Driver=localImage-backed volumes seed content from an OCI image. The Image= value can reference a .image Quadlet unit:
base.image:
[Image]
Image=registry.access.redhat.com/ubi9/ubi-minimalimgdata.volume:
[Volume]
VolumeName=img-data
Driver=image
Image=base.imageAfter daemon-reload, start the volume service:
sudo systemctl start imgdata-volume.serviceQuadlet adds an image dependency automatically:
systemctl cat imgdata-volume.serviceRelevant excerpt:
Requires=base-image.service
After=base-image.service
ExecStart=/usr/bin/podman volume create --ignore --driver image --opt image=registry.access.redhat.com/ubi9/ubi-minimal img-dataThe driver field confirms the image-backed volume:
sudo podman volume inspect img-data --format 'Driver={{.Driver}}'Sample output:
Driver=imageThis article does not survey every volume driver — see Podman volumes for copy-up behavior and permission patterns.
Copy=
Copy= controls initial copy-up from image content when a container first uses a volume:
[Volume]
VolumeName=copy-demo
Copy=falseThe default is true. Use Copy=false when you want an empty volume even if the mount path contained image files. Copy-up semantics are covered in Podman volumes — here the point is that Quadlet exposes the knob on the volume unit instead of repeating flags on every podman run.
Device=, Type=, and Options=
Local driver volumes can mount special filesystems by passing creation options Quadlet forwards to podman volume create:
[Volume]
VolumeName=tmp-demo
Driver=local
Type=tmpfs
Device=tmpfs
Options=size=64mStart the unit and inspect driver options:
sudo systemctl start tmp-volume.serviceRead back the driver options Podman stored:
sudo podman volume inspect tmp-demo --format 'Options={{json .Options}}'Sample output:
Options={"SIZE":"64m","device":"tmpfs","o":"size=64m","type":"tmpfs"}Options= is not new to Podman 6 — it maps driver-specific settings the same way you would pass --opt on the CLI. I did not include NFS or CIFS examples here because they need infrastructure this lab cannot reproduce accurately.
Create a minimal .network unit
[Network]
NetworkName=application-networkSave as app.network, reload, and start:
sudo systemctl daemon-reloadBring the network online:
sudo systemctl start app-network.serviceVerify the network:
sudo podman network inspect application-network --format 'Name={{.Name}}'Sample output:
Name=application-networkDefault naming without NetworkName=:
app.network → app-network.service → Podman network systemd-appReference .network from .container
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
Network=app.networkQuadlet passes --network application-network and generates Requires=app-network.service / After=app-network.service. You do not need a duplicate Requires= line in [Unit] unless you want ordering beyond the inferred network dependency.
Configure subnet and gateway
Static IPv4 layout belongs on the network unit:
[Network]
NetworkName=application-network
Subnet=10.20.0.0/24
Gateway=10.20.0.1After daemon-reload and systemctl start app-network.service, inspect the live network:
sudo podman network inspect application-network --format 'Subnet={{(index .Subnets 0).Subnet}} Gateway={{(index .Subnets 0).Gateway}}'Sample output:
Subnet=10.20.0.0/24 Gateway=10.20.0.1CIDR planning and bridge internals belong in Create Podman networks — Quadlet only declares what podman network create should receive.
Create an internal network
Internal networks restrict external access on bridge networks:
[Network]
NetworkName=internal-net
Internal=true
Subnet=10.60.0.0/24Start the unit and read the flag:
sudo systemctl start internal-network.serviceConfirm Podman marked the network internal:
sudo podman network inspect internal-net --format 'Internal={{.Internal}}'Sample output:
Internal=trueOn Netavark, internal mode changes routing, forwarding, and what aardvark-dns resolves — it is not simply “DNS off.” See Podman networking for how internal bridge networks behave with your backend.
Enable IPv6
Turn on dual-stack addressing with IPv6=true and explicit subnets:
[Network]
NetworkName=dual-net
Subnet=10.70.0.0/24
Subnet=fd00:70::/64
IPv6=trueList subnets after the service starts:
sudo systemctl start dual-network.servicePrint every subnet CIDR on the network:
sudo podman network inspect dual-net --format '{{range .Subnets}}{{.Subnet}} {{end}}'Sample output:
10.70.0.0/24 fd00:70::/64Repeat Subnet= for each CIDR when you need static IPv4 and IPv6 together.
Configure DNS
Disable the DNS plugin on a bridge network when container-to-container name resolution should not run:
[Network]
NetworkName=nodns-net
DisableDNS=true
Subnet=10.80.0.0/24Inspect the setting:
sudo systemctl start nodns-network.serviceRead whether the DNS plugin is enabled on that bridge:
sudo podman network inspect nodns-net --format 'DNSEnabled={{.DNSEnabled}}'Sample output:
DNSEnabled=falseSet upstream resolvers containers should see with DNS=:
[Network]
NetworkName=resolver-net
DNS=10.0.0.53
Subnet=10.90.0.0/24Resolver behavior inside running containers — overrides, DNS=. in podman run, and aardvark-dns forwarding — belongs in Podman container DNS.
NetworkDeleteOnStop=
By default, stopping the network service leaves the Podman network on disk:
[Network]
NetworkDeleteOnStop=falseThat conservative default lets supporting resources survive a temporary systemctl stop while you work on sibling units.
To delete the network when the service stops:
[Network]
NetworkName=ephemeral-net
NetworkDeleteOnStop=true
Subnet=10.50.0.0/24Start the service, confirm the network exists, then stop the service:
sudo systemctl start ephemeral-network.serviceWhile the service is active, the network should exist:
sudo podman network exists ephemeral-net && echo 'network present'Sample output:
network presentStop the network service to trigger deletion:
sudo systemctl stop ephemeral-network.serviceAfter stop, the network should be gone:
sudo podman network exists ephemeral-netThe command exits with status 1 and prints nothing when the network no longer exists — that is the expected result with NetworkDeleteOnStop=true.
Use true for lab networks you want torn down with the unit. Keep the default false for production networks that must outlive a service stop.
Updating existing networks
Quadlet network units ensure a network exists; they do not update an existing network's parameters. I demonstrated this with mutate.network:
[Network]
NetworkName=mutate-net
Subnet=10.40.0.0/24After the first start, the subnet was 10.40.0.0/24. I changed the file to 10.41.0.0/24, ran daemon-reload, and restarted:
sudo systemctl restart mutate-network.serviceInspect whether the subnet actually changed:
sudo podman network inspect mutate-net --format 'Subnet={{(index .Subnets 0).Subnet}}'Sample output:
Subnet=10.40.0.0/24The old subnet remained. To apply new settings, remove the network and start fresh:
sudo podman network rm mutate-netStart the service again so podman network create runs with the new subnet:
sudo systemctl start mutate-network.serviceOnly then did inspect show 10.41.0.0/24. Plan network changes as recreate operations, not in-place edits.
Full supporting-resource example
Three files wire storage and networking into one container service.
app.volume:
[Volume]
VolumeName=web-dataweb-net.network:
[Network]
NetworkName=web-net
Subnet=10.30.0.0/24web.container:
[Unit]
Description=Web app with Quadlet volume and network
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
ContainerName=quadlet-web-vn
Volume=app.volume:/data
Network=web-net.network
PublishPort=18081:8080
[Service]
Restart=on-failureReload systemd, then start only the container service:
sudo systemctl daemon-reloadStart only the container unit and let Quadlet pull up dependencies:
sudo systemctl start web.serviceQuadlet starts dependencies first — on the lab host all three units ended up active:
systemctl is-active web.service app-volume.service web-net-network.serviceSample output:
active
active
activeThe generated container unit shows inferred dependencies:
systemctl cat web.serviceRelevant excerpt:
Requires=web-net-network.service
After=web-net-network.service
Requires=app-volume.service
After=app-volume.service
ExecStart=/usr/bin/podman run ... --network web-net -v web-data:/data --publish 18081:8080 ...A quick host check confirms the published port reaches the container:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18081/Sample output:
403HTTP 403 means the listener answered — connection and port mapping work.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Container service fails with volume not found | Volume service never started or wrong VolumeName= |
Start data-volume.service manually; reference data.volume: in the container file, not the runtime name alone |
Requires= on network missing |
Used raw network name instead of Quadlet file reference | Set Network=app.network so the generator infers app-network.service |
| Subnet change ignored after edit | Existing Podman network not recreated | podman network rm <name>, then systemctl start <name>-network.service |
| Volume ownership wrong inside container | Confused [Volume] User= with [Container] User= |
Set host ownership on the volume unit; set process identity on the container unit |
Network gone after systemctl stop |
NetworkDeleteOnStop=true |
Use default false for persistent networks, or expect deletion when true |
| Image-backed volume fails to start | .image unit missing or not started |
Add base.image and verify Requires=base-image.service on the volume unit |
References
- podman-volume.unit(5) — Quadlet
.volumedirectives - podman-network.unit(5) — Quadlet
.networkdirectives includingNetworkDeleteOnStop - podman volume create — CLI flags behind
[Volume] - podman network create — CLI flags behind
[Network]
Summary
Quadlet .volume and .network files are supporting oneshot services — data.volume becomes data-volume.service and ensures a Podman volume exists before containers that reference Volume=data.volume:… start. The same pattern applies to networks: app.network becomes app-network.service, and Network=app.network on a container unit pulls in automatic Requires= and After= wiring.
Name resources deliberately with VolumeName= and NetworkName= when you do not want the default systemd-<filename> runtime objects. Set host ownership with [Volume] UID=/GID= or User=/Group= depending on whether you need mount-point ownership or mount options, and keep container process identity in the [Container] section. Use Driver=image with Image=base.image when you need image-seeded volumes, and treat NetworkDeleteOnStop as an explicit lifecycle choice.
The operational gotcha is network updates: changing Subnet=, Gateway=, or Internal= in a .network file does not reshape an existing network after daemon-reload. Remove the Podman network and restart the service when parameters change. For always-on stacks, wire app.volume, web-net.network, and web.container together and let systemd start only the container unit — dependencies follow from the Quadlet references.

