Podman Quadlet `.volume` and `.network` Units Explained

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:

bash
podman --version

Sample output:

output
podman version 5.8.2

How supporting Quadlet resources work

A volume Quadlet file creates a oneshot service that ensures a Podman volume exists:

text
data.volume
data-volume.service
Podman volume
web.container  (Volume=data.volume:/mount)

A network file follows the same pattern:

text
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:

ini
[Volume]
VolumeName=application-data

Save as data.volume under /etc/containers/systemd/, reload systemd, and start the generated service:

bash
sudo systemctl daemon-reload

Create the volume with the generated oneshot service:

bash
sudo systemctl start data-volume.service

The service should report active (exited) — oneshot units exit after podman volume create succeeds.

Confirm the volume exists:

bash
sudo podman volume inspect application-data --format 'Name={{.Name}}'

Sample output:

output
Name=application-data

Naming defaults without VolumeName=:

text
data.volume  →  data-volume.service  →  Podman volume systemd-data

VolumeName= 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:

ini
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
Volume=data.volume:/var/lib/app

Quadlet 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:

ini
[Volume]
VolumeName=application-data
User=1000
Group=1000

Reload and recreate if the volume already existed without ownership options:

bash
sudo systemctl daemon-reload

Stop 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:

bash
sudo systemctl stop data-volume.service
sudo podman volume rm application-data
sudo systemctl start data-volume.service

Creation-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:

bash
sudo podman volume inspect application-data --format 'UID={{.UID}} GID={{.GID}}'

Sample output:

output
UID=1000 GID=1000

The generated data-volume.service passes ownership into volume creation:

bash
systemctl cat data-volume.service

Relevant excerpt:

output
ExecStart=/usr/bin/podman volume create --ignore --opt o=uid=1000,gid=1000 application-data

Volume ownership keys vs container User= / Group=

These keys live in different sections and mean different things:

  • [Volume] UID= / GID= — mount-point directory ownership via podman 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 to podman volume create --uid and --gid
  • User= / Group= — passed as filesystem mount options on the volume mount

Example mappings:

text
UID=1234   → --uid 1234
GID=5678   → --gid 5678
User=123   → mount option o=uid=123
Group=192  → mount option o=group=192

The 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:

ini
[Volume]
VolumeName=web-data
Driver=local

Image-backed volumes seed content from an OCI image. The Image= value can reference a .image Quadlet unit:

base.image:

ini
[Image]
Image=registry.access.redhat.com/ubi9/ubi-minimal

imgdata.volume:

ini
[Volume]
VolumeName=img-data
Driver=image
Image=base.image

After daemon-reload, start the volume service:

bash
sudo systemctl start imgdata-volume.service

Quadlet adds an image dependency automatically:

bash
systemctl cat imgdata-volume.service

Relevant excerpt:

output
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-data

The driver field confirms the image-backed volume:

bash
sudo podman volume inspect img-data --format 'Driver={{.Driver}}'

Sample output:

output
Driver=image

This 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:

ini
[Volume]
VolumeName=copy-demo
Copy=false

The 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:

ini
[Volume]
VolumeName=tmp-demo
Driver=local
Type=tmpfs
Device=tmpfs
Options=size=64m

Start the unit and inspect driver options:

bash
sudo systemctl start tmp-volume.service

Read back the driver options Podman stored:

bash
sudo podman volume inspect tmp-demo --format 'Options={{json .Options}}'

Sample output:

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

ini
[Network]
NetworkName=application-network

Save as app.network, reload, and start:

bash
sudo systemctl daemon-reload

Bring the network online:

bash
sudo systemctl start app-network.service

Verify the network:

bash
sudo podman network inspect application-network --format 'Name={{.Name}}'

Sample output:

output
Name=application-network

Default naming without NetworkName=:

text
app.network  →  app-network.service  →  Podman network systemd-app

Reference .network from .container

ini
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
Network=app.network

Quadlet 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:

ini
[Network]
NetworkName=application-network
Subnet=10.20.0.0/24
Gateway=10.20.0.1

After daemon-reload and systemctl start app-network.service, inspect the live network:

bash
sudo podman network inspect application-network --format 'Subnet={{(index .Subnets 0).Subnet}} Gateway={{(index .Subnets 0).Gateway}}'

Sample output:

output
Subnet=10.20.0.0/24 Gateway=10.20.0.1

CIDR 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:

ini
[Network]
NetworkName=internal-net
Internal=true
Subnet=10.60.0.0/24

Start the unit and read the flag:

bash
sudo systemctl start internal-network.service

Confirm Podman marked the network internal:

bash
sudo podman network inspect internal-net --format 'Internal={{.Internal}}'

Sample output:

output
Internal=true

On 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:

ini
[Network]
NetworkName=dual-net
Subnet=10.70.0.0/24
Subnet=fd00:70::/64
IPv6=true

List subnets after the service starts:

bash
sudo systemctl start dual-network.service

Print every subnet CIDR on the network:

bash
sudo podman network inspect dual-net --format '{{range .Subnets}}{{.Subnet}} {{end}}'

Sample output:

output
10.70.0.0/24 fd00:70::/64

Repeat 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:

ini
[Network]
NetworkName=nodns-net
DisableDNS=true
Subnet=10.80.0.0/24

Inspect the setting:

bash
sudo systemctl start nodns-network.service

Read whether the DNS plugin is enabled on that bridge:

bash
sudo podman network inspect nodns-net --format 'DNSEnabled={{.DNSEnabled}}'

Sample output:

output
DNSEnabled=false

Set upstream resolvers containers should see with DNS=:

ini
[Network]
NetworkName=resolver-net
DNS=10.0.0.53
Subnet=10.90.0.0/24

Resolver 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:

ini
[Network]
NetworkDeleteOnStop=false

That conservative default lets supporting resources survive a temporary systemctl stop while you work on sibling units.

To delete the network when the service stops:

ini
[Network]
NetworkName=ephemeral-net
NetworkDeleteOnStop=true
Subnet=10.50.0.0/24

Start the service, confirm the network exists, then stop the service:

bash
sudo systemctl start ephemeral-network.service

While the service is active, the network should exist:

bash
sudo podman network exists ephemeral-net && echo 'network present'

Sample output:

output
network present

Stop the network service to trigger deletion:

bash
sudo systemctl stop ephemeral-network.service

After stop, the network should be gone:

bash
sudo podman network exists ephemeral-net

The 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:

ini
[Network]
NetworkName=mutate-net
Subnet=10.40.0.0/24

After 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:

bash
sudo systemctl restart mutate-network.service

Inspect whether the subnet actually changed:

bash
sudo podman network inspect mutate-net --format 'Subnet={{(index .Subnets 0).Subnet}}'

Sample output:

output
Subnet=10.40.0.0/24

The old subnet remained. To apply new settings, remove the network and start fresh:

bash
sudo podman network rm mutate-net

Start the service again so podman network create runs with the new subnet:

bash
sudo systemctl start mutate-network.service

Only 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:

ini
[Volume]
VolumeName=web-data

web-net.network:

ini
[Network]
NetworkName=web-net
Subnet=10.30.0.0/24

web.container:

ini
[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-failure

Reload systemd, then start only the container service:

bash
sudo systemctl daemon-reload

Start only the container unit and let Quadlet pull up dependencies:

bash
sudo systemctl start web.service

Quadlet starts dependencies first — on the lab host all three units ended up active:

bash
systemctl is-active web.service app-volume.service web-net-network.service

Sample output:

output
active
active
active

The generated container unit shows inferred dependencies:

bash
systemctl cat web.service

Relevant excerpt:

output
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:

bash
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18081/

Sample output:

output
403

HTTP 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


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.


Frequently Asked Questions

1. What Podman volume does data.volume create by default?

Without VolumeName, data.volume becomes data-volume.service and creates a Podman volume named systemd-data. Set VolumeName=application-data to override the runtime name while keeping the Quadlet filename for references.

2. What is the difference between Volume User= and Container User= in Quadlet?

Volume User= and Group= are passed as filesystem mount options, while Volume UID= and GID= set ownership of the volume mount-point directory. Container User= and Group= instead select the process identity inside the container.

3. Does stopping app-network.service delete the Podman network?

Not by default. NetworkDeleteOnStop defaults to false, so the network persists after the oneshot service stops. Set NetworkDeleteOnStop=true when you want the generated unit to remove the network on stop.

4. Can I change Subnet= in a network Quadlet and restart to update an existing network?

No. Quadlet network units ensure the network exists but do not mutate an already created network. Remove the Podman network manually, then restart the network service so podman network create runs with the new settings.

5. Should I use UID= and GID= in a volume Quadlet file?

UID= and GID= set the owner of the volume mount-point directory and map to podman volume create --uid/--gid. User= and Group= are different: Quadlet passes them to the filesystem mount operation. Choose the pair that matches the ownership behavior you need.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)