Docker volumes vs. bind mounts: where your data really lives
Named volume or bind mount? This foundational guide explains the difference, shows both with real examples, and when to use which.
Table of contents
A container is ephemeral: delete it, and its data is gone. For your database, your Nextcloud files or your configuration to survive a docker compose down, you need volumes. This guide clears up the often confusing difference between named volumes and bind mounts.
What are we building?
No new service this time, but a solid foundation: by the end you understand why container data normally disappears, know the two ways to store it permanently, and know for each service which to use. All examples are tested with Docker 29 on Debian 13 and work with any current Docker version. You need this knowledge for every app tutorial – there, a volumes: block appears in almost every compose.yaml.
Prerequisites
- A server with Docker installed
- Basic understanding of Docker Compose (services,
compose.yaml) - Terminal access to the server – a pure command-line exercise, no web interface
Step by step
Step 1: Why container data disappears
Everything a container writes into its own filesystem only lives as long as the container. That’s intended – containers are meant to be replaceable. For anything that should persist (databases, uploads, configuration), you have to tell Docker explicitly where it lands outside the container. That’s exactly what the two tools are for: named volumes and bind mounts.
Step 2: Named volumes – managed by Docker
A named volume is a storage area that Docker itself manages. You only give it a name; Docker determines the location. Create one:
docker volume create sk-demo-volsk-demo-volNow we mount the volume into a throwaway container (--rm deletes it afterwards) at /data and write a file into it:
docker run --rm -v sk-demo-vol:/data alpine:3 sh -c "echo hallo-von-serverkueche > /data/notiz.txt && ls -l /data"total 4
-rw-r--r-- 1 root root 23 Jul 20 22:31 notiz.txtThe container is long gone – the data isn’t. Where does it live? Ask Docker:
docker volume inspect sk-demo-vol --format "{{ .Mountpoint }}"/var/lib/docker/volumes/sk-demo-vol/_dataThis path belongs to Docker. You can view it as root, but shouldn’t edit it directly:
cat /var/lib/docker/volumes/sk-demo-vol/_data/notiz.txthallo-von-serverkuecheThe proof of persistence: a completely new container sees the same data:
docker run --rm -v sk-demo-vol:/data alpine:3 cat /data/notiz.txthallo-von-serverkuecheRemember: named volumes are the default for application data – databases, uploads, everything the app manages itself. Docker takes care of location and permissions, and the volume survives docker compose down (only down -v deletes it too).
Step 3: Bind mounts – a folder from the host
With a bind mount you mount a specific folder from your server into the container. You determine the path, and changes are immediately visible on both sides:
mkdir -p /opt/sk-demo-bind
docker run --rm -v /opt/sk-demo-bind:/data alpine:3 sh -c "echo aus-dem-container > /data/host.txt"
ls -l /opt/sk-demo-bindtotal 4
-rw-r--r-- 1 root root 18 Jul 21 00:31 host.txtThe container wrote the file, and it lives directly in your host folder – without the detour through /var/lib/docker. That’s the point of bind mounts: files you want to edit yourself. Classic cases are configuration files (traefik.yml, nginx.conf) or a compose.yaml that reads in a config.
Tip
Warning
/, not /etc, no home directories – and especially not /var/run/docker.sock. Anyone who gets the Docker socket mounted can start arbitrary containers and is thus effectively root on the entire host. Always mount only the one folder the service really needs.Step 4: Both in the compose.yaml
In Compose the difference looks like this. A named volume is declared at the bottom under volumes: and referenced by name above; a bind mount is simply a host path:
services:
app:
image: beispiel/app:1
volumes:
- app-daten:/var/lib/app # named volume (Docker-managed)
- ./config.yml:/etc/app/config.yml:ro # bind mount (your file, read-only)
volumes:
app-daten:The :ro at the end makes the bind mount read-only – the container can read the configuration but not change it. For mounted configs that’s a good habit.
Step 5: The trap – anonymous volumes
If you leave out the name with -v (-v /data instead of -v name:/data), or an image brings a VOLUME instruction in its Dockerfile, an anonymous volume with a random ID is created. This is what the first case looks like:
docker run --name demo -v /data alpine:3 sh -c "echo test > /data/x.txt"We deliberately run the container here without --rm – otherwise Docker would immediately delete the anonymous volume on exit. The everyday problem: such volumes stick around, pile up unnoticed, and you can no longer figure out later which data belongs where:
docker volume lsDRIVER VOLUME NAME
local sk-demo-vol
local 9f8c1a2b3c4d5e6f70819a0b1c2d3e4f5061a2b3c4d5e6f70819a0b1c2d3e4f50The cryptic line is an anonymous volume. You can now remove the throwaway container – the anonymous volume still stays behind:
docker rm demoAlways give your volumes a name – then it stays clear what belongs to what.
Step 6: Managing volumes
The most important everyday commands:
docker volume ls # list all volumes
docker volume inspect NAME # details, including the mountpoint
docker volume rm NAME # delete a volume (data gone!)We save cleaning up our demo until after the backup section – we still need sk-demo-vol there.
When things go wrong
The container writes, but the bind-mount folder on the host stays empty. You used a relative
path that points somewhere other than intended, or Docker created the path anew as an empty folder.
with docker run, always give bind mounts an absolute path (/opt/app/config, not config).
If this host path doesn’t exist yet, Docker silently creates it as an empty folder – so check with
ls that you really hit the right one. In the compose.yaml, ./ paths are perfectly fine, on the
other hand – they’re relative to the compose.yaml and thus unambiguous. Note also: docker run -v config:/data (without / or ./ in front) is not a bind mount, but creates a named volume
called config.
Permission denied as soon as the container tries to write into the mount. The process in the
container runs under a different UID than the owner of the host folder – typical with bind mounts.
with named volumes this rarely happens (Docker sets the permissions). With bind mounts, give the
folder to the matching user (chown -R 1000:1000 /opt/app/data) or use the user: setting of the
Compose in the image.
After docker compose down all data is gone. You used docker compose down -v – the -v
deletes the named volumes too. for a normal restart, work without -v. Use -v deliberately
only when you really want to start from scratch.
docker system prune deleted data. docker volume prune or docker system prune --volumes
removes volumes that no container is currently attached to. With docker volume prune, named
volumes are protected by default – only anonymous volumes are deleted; named ones only go with
--all/-a. (With docker system prune, on the other hand, -a/--all controls the images, not
the named volumes.) another strong argument for naming – an sk-demo-vol survives an accidental
docker volume prune, an anonymous volume doesn’t. Still, only run prune with volumes when all
important stacks are active, or remove specifically with docker volume rm.
Maintenance & backups
Volumes are not a backup – they live on the same disk as your server. If it fails, container and volume are gone. You back up a named volume by packing its contents into an archive via a throwaway container:
mkdir -p /opt/backups
docker run --rm -v sk-demo-vol:/data -v /opt/backups:/backup alpine:3 \
tar czf /backup/sk-demo-vol.tar.gz -C /data .Restoring works the same way around – archive in, unpack into the volume. Create the target volume beforehand (docker volume create sk-demo-vol) and stop the associated container so nothing writes into the open volume:
docker run --rm -v sk-demo-vol:/data -v /opt/backups:/backup alpine:3 \
tar xzf /backup/sk-demo-vol.tar.gz -C /dataThis archive – and all bind-mount folders under /opt – belong in your encrypted off-site backup with Restic. That way your database survives even a total failure of the server.
In everyday use it pays to stay tidy: name volumes descriptively (nextcloud-db, not db), clean up anonymous volumes occasionally, and check with docker system df how much space your volumes take up – with databases and media services in particular, this grows noticeably over time.
Finally, tear down the demo again – now that backup and all examples are done:
docker volume rm sk-demo-vol && rm -rf /opt/sk-demo-bind /opt/backupsSend feedback: feedback@serverkueche.de
You might also like

Understanding Docker Networks: bridge, internal DNS & the proxy network
Why does every app recipe declare a proxy network? This guide explains Docker networks, internal DNS and container …

Keeping your whole Docker stack safely up to date
Update discipline for your Docker stack: pin versions, get notified about updates with Diun, apply them safely, clean up …

Host Your Own Website with Hugo – Like Serverküche Itself
Build a static website with Hugo and serve it from a Docker container behind Traefik – fast, secure, no database. …