Volumes vs Bind Mounts: Making Data Outlive the Container
The writable layer dies with the container. Learn which storage option to reach for, the mount mistake that silently destroys a database, and how to verify it.
The Problem, Restated
From guide one of this roadmap: a container gets a thin writable layer on top of the image's read-only layers, and that writable layer is deleted when the container is removed.
For a stateless web server that's ideal — the container is disposable by design. For anything that remembers, it's a countdown timer. Every docker rm, every recreation to pick up a new image, every docker compose down takes the data with it.
Docker offers three ways to put data somewhere that survives.
The Three Options
| Volume | Bind mount | tmpfs | |
|---|---|---|---|
| Lives where | Docker-managed storage area | A path you choose on the host | Host memory |
| Survives container removal | Yes | Yes | No |
| Portable across machines | Yes | No — depends on host paths | n/a |
| Best for | Databases, uploads, any real state | Source code during development | Secrets, scratch data |
Volumes — the default choice for state
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:17Docker manages where the data actually sits. The container just knows a named volume is mounted at a path. Volumes are the right answer for databases, message queues, uploaded files — anything that is genuinely state.
They also survive docker rm, which is the point, and they can be backed up, inspected and moved between containers.
Bind mounts — the default choice for development
docker run -v "$(pwd)/src:/app/src" myappA specific host directory appears inside the container. Edit a file in your editor and the container sees the change immediately — which is what makes hot reload work in a containerised dev setup.
The tradeoff is portability: the mount depends on a path existing on this host with the right contents. That's fine for development and a liability in production.
tmpfs — for things that should never touch disk
docker run --tmpfs /tmp:size=64m myappBacked by memory, wiped when the container stops. Appropriate for decrypted secrets, session scratch space, or anything you'd rather not leave recoverable on disk.
The modern --mount syntax is more explicit than -v and worth preferring in anything you commit, because it fails loudly rather than guessing:
docker run --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:17
With -v, a typo in a host path silently creates a new empty directory. With --mount, it errors.
The Mistake That Destroys Databases
Here's the failure from this roadmap's scenarios, in detail — because it looks correct in review and fails silently.
services:
db:
image: postgres:17
volumes:
- pgdata:/data # wrong path
volumes:
pgdata:The volume is declared. It's attached. docker inspect confirms a mount exists. Everything about this passes a glance.
But Postgres writes to /var/lib/postgresql/data, not /data. So the volume sits there holding nothing, while the database writes its entire state into the writable layer — which is deleted on the next docker compose down.
Nothing warns you. The container is healthy, queries work, and the data is one recreation away from gone.
A mounted volume proves nothing on its own. What matters is whether it's mounted at the path the application actually writes to. Check the image's documentation for its data directory, then verify — don't infer it from the volume's name.
The verification takes ten seconds:
# What is actually mounted, and where?
docker inspect --format '{{json .Mounts}}' db | jq
# Is the application's data directory actually on the volume?
docker exec db df -h /var/lib/postgresql/dataIf df reports the same filesystem as /, the data is in the writable layer and the volume is decorative.
Check yourself
A Redis container is configured with a named volume mounted at /data, and the team assumes state is safe. After a redeploy, all keys are gone. Redis was running with default settings and no persistence configured. What went wrong?
The Linux Permissions Problem
A bind mount issue that doesn't reproduce on macOS and therefore ambushes people in CI.
On Linux, bind mounts preserve the host's numeric user and group IDs. If your container runs as UID 1000 and the mounted directory is owned by UID 501, the container gets permission errors writing to it. On Docker Desktop, the file-sharing layer papers over this, so the same setup works on a laptop and fails on a Linux build agent.
Options, roughly in order of preference:
# Run the container as the current host user
docker run -u "$(id -u):$(id -g)" -v "$(pwd):/app" myappOr align the container user's UID with the host's at build time, or — for state rather than source code — use a named volume, where Docker initialises ownership from the image and the problem doesn't arise.
Mounting over a directory hides whatever the image had there. A classic case: mounting your project root over /app when the image installed node_modules into /app/node_modules — your host directory has no node_modules, so the container now sees none either. The usual fix is an anonymous volume for that subdirectory (-v /app/node_modules) so it isn't shadowed by the bind mount.
Anonymous Volumes Accumulate
If an image's Dockerfile has a VOLUME instruction, or you use -v /some/path with no source, Docker creates an anonymous volume — a real volume with a random hexadecimal name.
They're easy to create by accident and invisible until you look:
docker volume ls # anonymous ones have long random names
docker system df -v # how much space each is using
docker volume prune # remove volumes not used by any containerRead docker volume prune carefully before running it. "Not used by any container" includes the volume holding your database from a container you removed an hour ago — the volume you were relying on to still be there. Check docker volume ls and confirm what's about to go.
Backing Up a Volume
Because a volume is Docker-managed, you back it up by mounting it into a throwaway container alongside a host directory:
# Back up
docker run --rm \
-v pgdata:/data:ro \
-v "$(pwd):/backup" \
alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .
# Restore
docker run --rm \
-v pgdata:/data \
-v "$(pwd):/backup" \
alpine sh -c "tar xzf /backup/pgdata-2026-09-13.tar.gz -C /data"For databases specifically, a logical dump (pg_dump, mysqldump) taken from the running container is usually safer than a file-level copy, which can capture a half-written state unless the database is stopped or quiesced.
A backup you have never restored is a hypothesis, not a backup. Run the restore once, into a scratch volume, and confirm the data is actually there. This is the single step most teams skip and most regret.
Check yourself
For a production Postgres container, which storage choice is most appropriate and why?
What's Next
Data now survives. The final guide of this phase covers what happens when a container stops — why yours takes exactly ten seconds, what PID 1 has to do with it, and how to write a health check that means something.