Three ways to persist data in a Docker container: named volumes, bind mounts, and tmpfs. They look similar at the CLI (-v something:/path/in/container), but they behave very differently and serve different jobs. Most "I lost my data" disasters trace back to picking the wrong one for the job.
The short version
| Mount type | What it is | Best for |
|---|---|---|
Named volume (-v name:/path) | Docker-managed storage at a path Docker chooses | Databases, app state, anything you want to persist and you don't care where it lives |
Bind mount (-v /host/path:/path) | A host directory mapped into the container | Editing source code from the host, host-side backups, sharing config files |
Anonymous volume (-v /path) | An unnamed Docker-managed volume | Almost never. Use a named volume instead. |
tmpfs (--tmpfs /path) | In-memory only | Scratch space, caches that don't need to survive container restart |
For databases and other stateful services, default to named volumes. For development where you're editing files on the host and want changes reflected in the container, bind mounts. For everything else, named volumes.
Named volumes
docker run -d -v pgdata:/var/lib/postgresql/data postgres:17Docker creates a volume named pgdata (if it doesn't exist) and mounts it at /var/lib/postgresql/data inside the container. The actual bytes live in /var/lib/docker/volumes/pgdata/_data/ on a Linux host, or inside the Docker Desktop VM on Mac/Windows.
The key properties:
- Survives container removal.
docker rmdoes not touch the volume. You can stop, remove, and recreate the container; the data is intact. - Surive even
docker compose down— but notdocker compose down -v(the-vflag explicitly removes named volumes). - Docker manages the on-disk location — you don't pick the path, you pick the name. Cleaner ownership, no UID surprises on Mac/Windows.
Manage them:
docker volume ls # list
docker volume inspect pgdata # driver, mountpoint, labels
docker volume rm pgdata # remove (must not be in use)
docker volume prune # remove all unused (data loss; asks for confirmation)Bind mounts
docker run -d -v /home/me/myproject:/app node:22 npm startMaps the host directory /home/me/myproject to /app inside the container. Both directions: changes inside the container modify the host directory; changes on the host appear inside the container.
The key properties:
- You choose the host path. Perfect for "this is my project; edit it on the host."
- Performance varies by platform. Native filesystem access on Linux is fast. Docker Desktop on Mac/Windows uses a virtualized filesystem that's slower, especially with thousands of small files.
- UID/GID matters. A container process writing to a bind mount creates files owned by whatever UID it ran as (root by default). Those files appear on the host with that ownership. Mac/Windows abstract this away to some extent; Linux exposes it directly.
Modes:
-v /host/path:/path:ro # read-only
-v /host/path:/path:rw # read-write (default)
-v /host/path:/path:z # SELinux relabel (private)
-v /host/path:/path:Z # SELinux relabel (shared)When to use which
Databases → named volume, always.
docker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres:17- The container's UID 999 (or whatever Postgres runs as) needs to own the data directory; named volumes handle this transparently.
- On Mac/Windows, bind-mounted database directories are noticeably slower and occasionally hit permission issues.
- You don't need direct host filesystem access to the database files; you'd use
pg_dumpinstead.
Source code in development → bind mount.
docker run -d -v "$(pwd):/app" -w /app node:22 npm run dev- You edit on the host with your editor, the container picks up changes immediately for hot reload.
- Files created by the container appear on the host (a build output, a generated migration). That's usually what you want.
The hybrid: bind-mount source + named volume for node_modules.
services:
app:
image: node:22
volumes:
- .:/app
- app-node_modules:/app/node_modules
volumes:
app-node_modules:This is the trick that keeps dev fast on Mac/Windows. The source bind mount gives you live editing; the named volume overlay on node_modules keeps that directory in a Docker-managed location instead of the slow bind-mount space. Without this, npm install and any tool that walks node_modules is dramatically slower.
Config files → bind mount, read-only.
docker run -d -v ./nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:alpine- One file, edited on the host, mounted into the container.
:roprevents the container from modifying your host file.
Caches and tmpfiles → tmpfs.
docker run -d --tmpfs /tmp:size=100m my-app- Lives in RAM, not on disk. Disappears when the container stops.
- Useful for scratch space or temp files that shouldn't survive a restart.
Backups
Named volume backup:
docker run --rm \
-v pgdata:/source:ro \
-v "$(pwd):/backup" \
alpine tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /source .That spins up a throwaway alpine container with the volume mounted at /source and the host's current directory at /backup, then tars the volume contents to a host file.
Restore:
docker run --rm \
-v pgdata:/restore \
-v "$(pwd):/backup" \
alpine tar xzf /backup/pgdata-20260601.tar.gz -C /restoreFor databases specifically, prefer the database's own dump tool (pg_dump, mysqldump, mongodump) — those produce portable, version-independent backups. Volume-level backups require stopping the container for consistency and are tied to the specific database version.
Where named volumes live on disk
- Linux:
/var/lib/docker/volumes/<name>/_data - macOS: inside Docker Desktop's virtual disk (
~/Library/Containers/com.docker.docker/Data/...). Not directly accessible from macOS. - Windows: inside Docker Desktop's WSL2 distro's filesystem (
\\wsl$\docker-desktop-data\...).
Full picture for macOS in Where Are Docker's Files on a Mac?.
Permission errors on bind mounts (and how to avoid them)
The container's UID writing to a bind mount creates files owned by that UID on the host. By default, that UID is root. On a Linux host, ls -la shows the file as owned by root; you need sudo rm to delete it.
Two fixes:
# Run the container as your host user
docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app node:22 npm test# After the fact (Linux)
sudo chown -R $(id -u):$(id -g) .This is why named volumes are nicer for stateful services on Mac/Windows — they sidestep the UID question entirely.
Common pitfalls
- Using an anonymous volume by accident.
-v /var/lib/postgresql/data(no name on the left of the colon) creates an anonymous volume. It's persistent, but you can't easily reference it later. Use a named volume. docker compose down -vwiping data. The-vflag removes named volumes. Don't run it casually on stacks with state you care about.- Bind mount over a directory that contains useful image content. Mounting
./empty-dir:/app/wp-contenton a WordPress container overlays the image's defaultwp-content, hiding the bundled themes. Named volumes copy the image content into the volume on first init; bind mounts do not. - Permission flips on Mac. Docker Desktop's file sharing handles the UID translation transparently but occasionally produces odd ownership. Restart Docker Desktop, or move to a named volume.
- Trusting a volume to survive everything.
docker volume rmanddocker volume pruneboth remove volumes. The volume is durable across container removal, not across explicit volume operations.
What to do next
- How to Write a Dockerfile — fundamentals.
- Docker Compose: Getting Started — how volumes are declared in Compose.
- How to Run MySQL in Docker, PostgreSQL, MongoDB — concrete persistence recipes per database.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Volumes — Docker docsdocs.docker.com
- Bind mounts — Docker docsdocs.docker.com


