Container volumes and networks
Listen to this lesson
This episode is a study companion for CompTIA Linux+ XK0-006 and is not produced by or endorsed by CompTIA.
Why this matters
A container's filesystem is disposable by design. That is a feature — you can throw one away and start an identical replacement — and it means any data you care about must live outside the container. Getting that wrong destroys a database the first time somebody runs podman rm.
Networking decides what containers can reach and what can reach them. The default works for almost everything; the alternatives exist for specific situations, and choosing one you do not need is a common way to make a simple deployment complicated.
The lesson
Why volumes exist
A running container has a thin writable layer on top of its read-only image. Everything it writes goes there, and that layer is deleted with the container. Stop and remove a database container without a volume and the data is gone — not corrupted, not recoverable, gone.
Volumes attach storage that outlives the container.
Volume operations
podman volume create pgdata
podman volume ls
podman volume inspect pgdata # where it lives on the host
podman volume rm pgdata
podman volume prune # remove all unused volumes
Two ways to attach storage, and the distinction matters:
Named volumes — managed by the runtime, stored under its own directory:
podman run -d -v pgdata:/var/lib/postgresql/data postgres:16
Portable, easy to back up as a unit, and the right default for application data. The container does not care where on the host it actually sits.
Bind mounts — a specific host path mapped in:
podman run -d -v /srv/config:/etc/app:ro myapp # read-only
podman run -d -v /srv/site:/usr/share/nginx/html:ro nginx
podman run -d --mount type=bind,src=/srv/data,dst=/data myapp
Right for configuration you edit on the host, for source code during development, and for anything already living in a specific place. Less portable, because the path must exist on every host you run it on.
:ro costs nothing and should be the default for configuration. A container that only reads its configuration should not be able to rewrite it.
Backing up a named volume is a container job, since the data is not conveniently on your path:
podman run --rm -v pgdata:/data -v $(pwd):/backup alpine \
tar czf /backup/pgdata.tar.gz -C /data .
For a database, prefer the application's own dump — the same live-file consistency point as the backup lesson. A tar of a running database's data directory may not restore.
SELinux and volumes
On RHEL-family systems this is the single most common container storage failure, and the symptom is unhelpful: permission denied on a bind mount whose permissions are plainly correct.
The cause is SELinux. Host files carry a type the container's process type may not read. The fix is a mount-option suffix:
podman run -d -v /srv/data:/data:Z myapp # PRIVATE label, this container only
podman run -d -v /srv/data:/data:z myapp # SHARED label, several containers
:Z relabels the host directory to a label only this container can use. :z relabels it shared, so more than one container can access it.
Use the lower-case z deliberately: capital Z on a directory two containers need means the second one is denied. And never apply either to a broad system path — -v /etc:/host-etc:Z relabels your actual /etc and can break the host, which is a genuinely bad afternoon.
Rootless Podman adds a second, separate cause of the same symptom: user namespaces map container UIDs to a range of host UIDs, so a file owned by your user may not be writable by the container's mapped UID. --userns=keep-id maps your own UID through, which resolves it.
Underneath all of this the container's filesystem is an overlay — the read-only image layers plus the writable layer, unified by OverlayFS. podman inspect shows the graph driver, and understanding that the writable layer is a separate overlay is what makes "data disappears on rm" intuitive rather than surprising.
Container networks
podman network ls
podman network create appnet
podman network create --subnet 10.89.0.0/24 appnet
podman network inspect appnet
podman network connect appnet web
podman network disconnect appnet web
podman network rm appnet
podman network prune
Create a network for each application. Containers on a user-defined network can resolve each other by name — the runtime runs a small DNS server for it — so an application container reaches its database at db:5432 with no addresses anywhere in the configuration:
podman network create appnet
podman run -d --name db --network appnet -v pgdata:/var/lib/postgresql/data postgres:16
podman run -d --name app --network appnet -e DB_HOST=db -p 8080:8080 myapp
Name resolution does not work on the default bridge network, only on user-defined ones. That is the practical reason to create one.
Network types
bridge — the default. A private virtual network on the host; containers get internal addresses and reach the outside through NAT. Inbound requires an explicit port mapping.
host — no isolation at all. The container shares the host's network stack, so a service on port 80 in the container is on port 80 of the host, and -p is meaningless. Faster, because nothing is translated, and appropriate for high-throughput network tools — at the cost of the isolation you probably wanted.
none — no networking whatsoever. Correct for a batch job that processes a mounted volume and must not talk to anything. The strongest containment available in one flag.
macvlan — the container gets its own MAC address and appears as a separate physical device on the LAN, with an address from the LAN's DHCP. Useful when something on the network must reach the container directly, or when an application needs a real LAN presence. Two caveats: many wireless networks reject multiple MACs on one interface, and by default the host cannot talk to its own macvlan containers, which surprises people.
ipvlan — similar, but containers share the parent interface's MAC and are distinguished by IP. It avoids the multiple-MAC problem, and works where switch port security would block macvlan.
overlay — a network spanning multiple hosts, so containers on different machines communicate as though on one network. This is the multi-host case, used by Docker Swarm and Kubernetes; you will not configure it on a single machine.
podman run -d --network host nginx
podman run --network none alpine ls
podman network create -d macvlan --subnet 192.168.1.0/24 \
--gateway 192.168.1.1 -o parent=eth0 lannet
bridge unless you have a specific reason is the right default, and the same shape of advice as the VM networking lesson.
Port mapping
podman run -d -p 8080:80 nginx # host 8080 → container 80
podman run -d -p 127.0.0.1:8080:80 nginx # bind to loopback only
podman run -d -p 8080:80/udp myapp
podman run -d -P nginx # map all EXPOSEd ports to random ones
podman port web
Host first, container second.
-p 8080:80 binds to 0.0.0.0 — every interface, reachable from the network. That is frequently not intended. A database or admin interface that should only be reached locally, or through a reverse proxy, wants -p 127.0.0.1:5432:5432. Publishing a database container's port with the short form exposes it to anything that can reach the host, which is one of the more common accidental exposures in container deployments.
Privileged versus unprivileged
podman run --privileged myapp # almost never correct
podman run --cap-add=NET_ADMIN myapp # one capability
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
podman run --security-opt no-new-privileges myapp
podman run --read-only --tmpfs /tmp myapp
podman run --user 1000:1000 myapp
--privileged disables essentially all container isolation. It grants every capability, allows access to all host devices, and turns off seccomp and SELinux confinement. A privileged container is close to a root shell on the host, and escaping one is not difficult. It appears in Stack Overflow answers because it makes things work — and it makes them work by removing the security model.
The correct approach is to identify the specific capability needed and add only that. NET_ADMIN for network configuration, SYS_TIME for setting the clock, NET_BIND_SERVICE to bind below port 1024. Better still, drop everything and add back:
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Rootless containers — Podman's default — are the larger win. The container runs as your unprivileged user through a user namespace, so "root" inside is a mapped, powerless UID outside. An escape lands the attacker as you rather than as root. The limitations are real but narrow: binding ports below 1024 needs configuration, and some storage drivers behave differently.
--read-only with a tmpfs for the few writable paths is a strong addition: an attacker who gets code execution cannot persist anything.
On the exam
- Data outside a volume is deleted with the container.
- Named volumes are runtime-managed and portable; bind mounts map a host path. Use
:rofor configuration. -
:Zand:zfix SELinux denials on bind mounts —Zprivate to one container,zshared. Never apply them to system paths. - Containers on a user-defined network resolve each other by name; the default bridge does not do this.
-
hostshares the host stack and ignores-p;nonehas no networking;macvlangives a MAC on the LAN;overlayspans hosts. -
-p 8080:80binds all interfaces — use-p 127.0.0.1:8080:80to keep it local. -
--privilegedremoves the isolation model. Add specific capabilities instead, or drop all and add back. - Rootless means an escape yields an unprivileged user, not root.
Practise what you just read
1. A database container is removed with podman rm. Its data was written to /var/lib/postgresql inside the container with no volume attached. What is the outcome?
Select one
Show answer
A. A running container has one thin writable layer over its read-only image, and that layer is destroyed with the container -- not corrupted, not recoverable, gone. This is the most common serious container mistake and the entire reason volumes exist. Anything you care about must live outside the container.
2. Two containers on the default bridge network cannot reach each other by name. Why?
Select one
Show answer
C. A user-defined network runs a small DNS server so containers on it resolve each other by name, which is why an application reaches its database at db:5432 with no addresses in the configuration. The default bridge does not do this. Creating a network per application is the practical reason to bother.
3. What does --privileged do to a container?
Select one
Show answer
C. A privileged container is close to a root shell on the host, and escaping one is not difficult. It appears in answers online because it makes things work -- by removing the security model. Identify the specific capability needed and add only that, or drop all and add back: --cap-drop=ALL --cap-add=NET_BIND_SERVICE.
8 more questions on this objective are part of the full course.
Hands-on labs
Part of the free CompTIA Linux+ XK0-006 course — 48 lessons and 82 hands-on labs.