Anything a container writes to its own filesystem is destroyed when the container is removed. Volumes are how data outlives the container that created it.
This catches essentially everyone once. A database container is recreated, and the data is gone.
A container has a thin writable layer on top of the read-only image layers. It exists for the life of that container and is deleted with it.
docker run -d --name db postgres:17
# ... write some data ...
docker rm -f db
docker run -d --name db postgres:17 # empty. The data was in the old writable layer.Note that docker stop and docker start preserve the writable layer, only docker rm destroys it. But since replacing containers is the normal way to deploy a new image, anything valuable must live outside.
| Type | Managed by | Best for |
|---|---|---|
| Named volume | Docker | Persistent application data |
| Bind mount | You, via a host path | Development, mounting host config |
| tmpfs | Memory only | Secrets and scratch that must never touch disk |
# Named volume
docker run -d -v pgdata:/var/lib/postgresql/data postgres:17
# Bind mount
docker run -d -v $(pwd)/src:/app/src myapp:dev
# tmpfs
docker run -d --tmpfs /tmp:size=64m myappThe modern --mount syntax is more explicit and worth preferring in scripts, since -v silently creates a directory when a host path does not exist rather than erroring:
docker run -d --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:17
docker run -d --mount type=bind,source="$(pwd)"/src,target=/app/src,readonly myapp:devDocker manages the location, the lifecycle and the permissions:
docker volume create pgdata
docker volume ls
docker volume inspect pgdataThey live under /var/lib/docker/volumes/ on Linux and inside the Docker Desktop VM on macOS and Windows, which is deliberate, because not caring where they live is the point.
To look inside one without a running container:
docker run --rm -v pgdata:/data alpine ls -la /dataUse named volumes for application data. They are portable between hosts, behave identically across platforms, and perform far better than bind mounts on macOS and Windows.
A direct mapping of a host path into the container:
docker run -d -v $(pwd):/app -v /app/node_modules myapp:devThe standard development pattern: mount your source so edits appear instantly, and use that second anonymous volume to shadow node_modules. Otherwise the host's empty or platform-mismatched directory hides the one installed during the build.
Two things to know.
They are slow on macOS and Windows. Every file access crosses into the Docker VM, which is painful for projects with many small files. Mount only what you need, keep dependency directories in named volumes, and enable VirtioFS in Docker Desktop.
Mount read-only where you can:
docker run -v /host/config:/etc/app:ro myappOn Linux, a container running as root writes files owned by root onto your host:
$ ls -l output/
-rw-r--r-- 1 root root 1024 Sep 16 10:00 report.csvThe container preserves real UIDs rather than remapping them. Fix it by running as your own user:
docker run --user $(id -u):$(id -g) -v $(pwd):/app myappThe reverse also happens: a container running as a non-root user cannot write to a host directory owned by you, producing permission denied. Matching the UID is the reliable fix.
This is Linux-specific. Docker Desktop on macOS and Windows handles the mapping.
For named volumes Docker sets ownership from the image's declared user on first use, which is why they largely avoid this class of problem.
There is no built-in backup command. The pattern is a throwaway container mounting both the volume and a host directory:
# Back up
docker run --rm -v pgdata:/data -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 \
tar xzf /backup/pgdata-2026-09-16.tar.gz -C /dataFor a database, prefer the engine's own dump tool. Copying live database files can capture a torn, inconsistent state:
docker exec db pg_dump -U postgres app > backup.sqlVolumes are excluded from docker system prune by default, deliberately, because they hold the data you least want to lose:
docker system df -v # usage per volume
docker volume prune # remove volumes no container references
docker system prune --volumes # include them in a general cleanThe hidden consumer is anonymous volumes. An image declaring VOLUME /data creates a fresh unnamed volume on every docker run if you do not mount one there, so repeatedly starting a database image leaves a trail of orphaned volumes holding real data.
docker volume ls -f dangling=trueAvoid them by always mounting a named volume explicitly, and by using --rm, which removes anonymous volumes on exit.
| Sub-topic | What you will learn |
|---|---|
| Named Volumes | Creating, sharing and backing them up |
| Bind Mounts | Development workflows and permissions |
| Volume Management | Inspecting, pruning and migrating |
| Task | Command |
|---|---|
| Create | docker volume create <name> |
| List | docker volume ls |
| Inspect | docker volume inspect <name> |
| Mount a named volume | docker run -v <name>:/path <image> |
| Bind mount | docker run -v $(pwd):/app <image> |
| Read-only | docker run -v /host:/path:ro <image> |
| Look inside | docker run --rm -v <name>:/data alpine ls /data |
| Which container uses it | docker ps -a --filter volume=<name> |
| Usage per volume | docker system df -v |
| Remove unused | docker volume prune |
docker stop preserves it; docker rm destroys it.--user $(id -u):$(id -g) fixes it.node_modules with an anonymous volume when bind-mounting a project directory.docker system prune skips volumes unless you pass --volumes.VOLUME instruction.<MCQExercise exercise={{ id: 'docker-vol-data-lost', title: 'A database that keeps forgetting', description: 'A Postgres container is recreated after an image update and all data is gone. It was started with docker run -d postgres:17 and no volume. What happened?', options: [ { id: 'a', text: 'The data lived in the container writable layer, which is destroyed when the container is removed. It needed a named volume mounted at the data directory' }, { id: 'b', text: 'Updating the image format wiped the existing data files' }, { id: 'c', text: 'Postgres clears its data directory on every start unless told otherwise' }, { id: 'd', text: 'The data is still there but needs docker volume restore to recover it' }, ], correctId: 'a', explanation: 'Without an explicit mount, everything Postgres writes goes to the container writable layer, which exists only for that container. Removing it to deploy a new image deletes the data with it. Mounting a named volume at /var/lib/postgresql/data keeps the data outside the container so replacements reattach to it. Note the official image declares a VOLUME there, so an anonymous volume may still hold the old data. Findable with docker volume ls.', wrongExplanations: { b: 'Image updates never touch data in a mounted volume, and here there was no volume to touch.', c: 'Postgres initialises only an empty data directory. It preserves existing data on every start.', d: 'No such command exists, and there is nothing to restore. The writable layer was deleted with the container.', }, }} />
<MCQExercise exercise={{ id: 'docker-vol-prune', title: 'Pruning that left the volumes', description: 'A server is short on disk. docker system prune -a frees some space but docker volume ls still shows dozens of volumes. Why were they not removed?', options: [ { id: 'a', text: 'Volumes are deliberately excluded from system prune. They hold data, so removing them requires --volumes or docker volume prune' }, { id: 'b', text: 'Volumes can only be removed while the Docker daemon is stopped' }, { id: 'c', text: 'The -a flag removes images only and has no volume equivalent' }, { id: 'd', text: 'Named volumes are never removable once created' }, ], correctId: 'a', explanation: 'Docker treats volumes as the most valuable thing on the host and will not delete them as part of a general cleanup, since an unused volume today may be a database you are about to reattach. Use docker volume prune for volumes with no container referencing them, or system prune --volumes. Check docker system df -v first, because deletion is permanent with no recovery.', wrongExplanations: { b: 'Volumes are managed through the daemon and can only be pruned while it is running.', c: '--volumes exists precisely for this. The -a flag widens image removal rather than covering volumes.', d: 'Named volumes are removable with docker volume rm or prune. They are simply excluded from the default cleanup.', }, }} />
A named volume is managed by Docker, referenced by name, and stored somewhere Docker chooses. A bind mount maps a specific host directory into the container. Use named volumes for persistent application data. They are portable across hosts, handle permissions better, and are considerably faster on macOS and Windows. Use bind mounts for development, where you want host edits reflected in the container immediately, and for mounting host configuration read-only.
On Linux, the container preserves real user IDs rather than remapping them, so a container running as a non-root user cannot write to a host directory owned by you, and a root container creates root-owned files on your host. Run the container as your own user with docker run --user $(id -u):$(id -g), or set a matching USER in the Dockerfile. Named volumes largely avoid this because Docker sets ownership from the image's declared user on first use.
Use a throwaway container that mounts both the volume and a host directory, then archive one into the other: docker run --rm -v myvol:/data -v $(pwd):/backup alpine tar czf /backup/vol.tar.gz -C /data .. There is no built-in backup command. For databases, use the engine's own dump tool instead, pg_dump, mysqldump, because copying live database files can capture an inconsistent state that will not restore cleanly.
Because volumes are excluded by default, deliberately, since they usually contain the data you would least want removed by a routine cleanup. Use docker volume prune to remove volumes no container references, or docker system prune --volumes to include them. Check docker system df -v first to see which volumes are actually consuming space, and be aware the deletion is permanent.
When an image declares VOLUME /data in its Dockerfile and you run it without mounting anything at that path, Docker creates an unnamed volume with a random hexadecimal ID. Every docker run creates another, so repeatedly starting a database image leaves a trail of orphaned volumes each holding real data. Always mount a named volume explicitly at the documented path, and add --rm to one-off runs, which removes anonymous volumes when the container exits.
Create Docker named volumes with docker volume create, -v and --mount. See how data survives container removal and how to share a volume.
Learn how to use a Docker bind mount with -v and --mount to share a host directory, make it read-only with :ro, and set up live reload for development.
Manage Docker volumes with docker volume ls, inspect, rm and prune. Learn how to back up a volume, restore it from a tar archive and migrate data between hosts.