Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Basics Difficulty: Beginner

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.

· 7 min read ·Duration: approx. 25 minutes
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:

Terminal
docker volume create sk-demo-vol
Ausgabe
sk-demo-vol

Now we mount the volume into a throwaway container (--rm deletes it afterwards) at /data and write a file into it:

Terminal
docker run --rm -v sk-demo-vol:/data alpine:3 sh -c "echo hallo-von-serverkueche > /data/notiz.txt && ls -l /data"
Ausgabe
total 4
-rw-r--r--    1 root     root            23 Jul 20 22:31 notiz.txt

The container is long gone – the data isn’t. Where does it live? Ask Docker:

Terminal
docker volume inspect sk-demo-vol --format "{{ .Mountpoint }}"
Ausgabe
/var/lib/docker/volumes/sk-demo-vol/_data

This path belongs to Docker. You can view it as root, but shouldn’t edit it directly:

Terminal
cat /var/lib/docker/volumes/sk-demo-vol/_data/notiz.txt
Ausgabe
hallo-von-serverkueche

The proof of persistence: a completely new container sees the same data:

Terminal
docker run --rm -v sk-demo-vol:/data alpine:3 cat /data/notiz.txt
Ausgabe
hallo-von-serverkueche

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

Terminal
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-bind
Ausgabe
total 4
-rw-r--r-- 1 root root 18 Jul 21 00:31 host.txt

The 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

Rule of thumb: named volume for data the app manages (database, uploads). Bind mount for files you manage (configuration). When in doubt, named volume – it causes the fewest permission problems.

Warning

Never blindly mount sensitive host paths into a container: not /, 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:

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

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

Terminal
docker volume ls
Ausgabe
DRIVER    VOLUME NAME
local     sk-demo-vol
local     9f8c1a2b3c4d5e6f70819a0b1c2d3e4f5061a2b3c4d5e6f70819a0b1c2d3e4f50

The cryptic line is an anonymous volume. You can now remove the throwaway container – the anonymous volume still stays behind:

Terminal
docker rm demo

Always give your volumes a name – then it stays clear what belongs to what.

Step 6: Managing volumes

The most important everyday commands:

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

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

Terminal
docker run --rm -v sk-demo-vol:/data -v /opt/backups:/backup alpine:3 \
  tar xzf /backup/sk-demo-vol.tar.gz -C /data

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

Terminal
docker volume rm sk-demo-vol && rm -rf /opt/sk-demo-bind /opt/backups

You might also like