Docker Volumes, Bind Mounts, and Networks Explained
Two things trip up almost everyone after their first docker run: data disappearing when a container is recreated, and containers that cannot reach each other. Both come down to how Docker handles storage and networking. This guide covers both, picking up where our Docker and Podman basics and Docker Compose basics leave off.
Part 1: storage
A container’s own filesystem is a thin writable layer on top of the image. When the container is removed, that layer is deleted, and with it anything written there. Updating an app usually means removing and recreating its container, so anything you want to keep must live outside it.
Docker offers three kinds of mount:
| Type | What it is | Best for |
|---|---|---|
| Named volume | Storage managed by Docker, referred to by name | Databases, app data you rarely touch directly |
| Bind mount | A specific host path mapped into the container | Config files, media libraries, anything you edit from the host |
| tmpfs | Memory only, gone when the container stops | Caches and secrets that must not touch disk |
Named volumes
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:17
docker volume ls
docker volume inspect pgdata
Docker stores volumes under /var/lib/docker/volumes/. They are created automatically the first time a container uses one, and if the image has files at that path, Docker copies them into a new empty volume, which is handy for default configs.
Bind mounts
docker run -d --name web \
-v /srv/site:/usr/share/nginx/html:ro \
nginx
The :ro flag makes it read-only inside the container, which is a good default for anything the container should not modify.
In Compose:
services:
app:
image: ghcr.io/example/app:latest
volumes:
- app-data:/data # named volume
- ./config:/config:ro # bind mount, relative to the compose file
volumes:
app-data:
The docker compose generator and docker run builder help write these.
Permissions
The most common bind mount problem is permission denied. Inside the container, the process runs as some user ID, and the kernel checks that numeric UID against the host directory’s ownership. If the container runs as UID 1000 and the directory belongs to root, it fails.
Fixes, in order of preference:
- Use the image’s PUID/PGID variables if it has them (LinuxServer.io images do), set to your user:
-e PUID=1000 -e PGID=1000 - Change the host directory’s ownership to match:
sudo chown -R 1000:1000 /srv/app - Run the container as your user:
--user 1000:1000
Our users, groups, and ownership guide explains why numbers, not names, are what count.
SELinux
On Fedora, RHEL, and other SELinux systems, bind mounts are blocked unless labelled. Add a suffix:
:Z: relabel for this container only (private):z: relabel for shared use by several containers
docker run -v /srv/app:/data:Z myimage
Never use :Z on system directories like /home or /etc; relabelling them breaks the host. See SELinux explained.
Backing up volumes
For databases, use the application’s dump tool, not a file copy of a running database:
docker exec db pg_dump -U postgres mydb > mydb.sql
For other volumes, stop the app, then archive the volume through a temporary container:
docker run --rm -v app-data:/data -v "$PWD":/backup alpine \
tar czf /backup/app-data.tar.gz -C /data .
Restore the same way with tar xzf. For automated, versioned backups, see restic vs borg and docker-volume-backup.
Part 2: networking
The network types
| Driver | Behaviour |
|---|---|
| bridge (default) | Containers get private IPs on a virtual bridge; reach the outside world through NAT |
| user-defined bridge | Same, plus DNS by container name and better isolation |
| host | No isolation: the container uses the host’s network stack directly |
| none | No networking |
| macvlan | The container gets its own MAC and IP on your LAN |
Our container networking explainer and VLANs and bridges guide cover what happens underneath.
Use user-defined networks
On the default bridge, containers can only reach each other by IP address, which changes. On a user-defined network, Docker’s embedded DNS resolves container names:
docker network create backend
docker run -d --name db --network backend postgres:17
docker run -d --name app --network backend -e DB_HOST=db myapp
app reaches the database simply as db.
Docker Compose does this automatically: each project gets its own network, and services reach each other by service name. You can also split services across networks so that, for example, only the app can reach the database:
services:
proxy:
networks: [frontend]
app:
networks: [frontend, backend]
db:
networks: [backend]
networks:
frontend:
backend:
Publishing ports
-p 8080:80 makes container port 80 reachable on host port 8080, on every interface. That is often not what you want.
If a reverse proxy on the same host serves the app, publish on localhost only:
docker run -p 127.0.0.1:8080:80 myapp
ports:
- "127.0.0.1:8080:80"
Better still, put the proxy on the same Docker network and do not publish the app’s port at all.
The firewall surprise
Docker manages its own iptables/nftables rules for published ports, and they are evaluated before ufw’s rules. So a port published with -p 8080:80 is reachable from the internet even if ufw denies 8080.
The fixes:
- Publish on 127.0.0.1 when only local access is needed (the simplest and most reliable fix)
- Do not publish ports for services that only other containers need
- Use the
DOCKER-USERchain for custom rules that Docker respects
Our firewall basics and nftables vs iptables guides explain the chains involved. Then verify from another machine with nmap that only what you intended is exposed.
Podman notes
Podman uses the same -v and --network syntax. Rootless Podman maps container users through user namespaces, which changes how bind mount ownership appears; :U can adjust ownership automatically. See rootless containers explained and Podman Quadlet.
Frequently Asked Questions
What is the difference between a Docker volume and a bind mount?
A named volume is storage managed by Docker, kept under /var/lib/docker/volumes, and referred to by name. A bind mount maps a specific path on the host into the container. Volumes are more portable and handle permissions more predictably; bind mounts make it easy to edit files from the host, such as configuration.
Does data in a container disappear when it is removed?
Data written to the container’s own filesystem does. Data written to a volume or bind mount survives removing and recreating the container, which is why anything you want to keep, such as databases and uploads, must be stored in one.
Why do I get permission denied on a bind mount?
The process in the container runs as a user ID that does not have access to the host directory, or SELinux is blocking access. Match the ownership to the container’s user ID, use the image’s PUID and PGID options if it has them, and on SELinux systems add :Z or :z to the mount.
How can containers talk to each other by name?
Put them on the same user-defined network. Docker runs an internal DNS server on user-defined networks, so a container can reach another one by its container or service name. Docker Compose creates such a network for each project automatically. The default bridge network does not provide name resolution.
Why can people reach a published port even though ufw blocks it?
Docker inserts its own firewall rules for published ports, and they are evaluated before ufw’s rules, so ufw does not see that traffic. Publish ports only on 127.0.0.1 when a reverse proxy on the same host will serve them, or use Docker’s own firewall configuration to restrict access.
How do I back up a Docker volume?
Stop the container, or use the application’s own dump tool for databases, then archive the volume through a temporary container that mounts it, for example with docker run —rm and tar. You can also back up the directory under /var/lib/docker/volumes directly as root while the application is stopped.