Docker volume vs bind mount: what is the difference?
Volumes and bind mounts are two ways Docker can give a container access to data outside its image layers. They look similar in usage but differ in what they couple you to and how Docker treats them.
Theory
TL;DR
- Named volume = Docker-managed directory with a logical name. Lives under
/var/lib/docker/volumes/<name>/_data. Created/destroyed by Docker. - Bind mount = direct mapping of a host path (any path, anywhere) into the container. Docker only mounts; the host filesystem owns the lifecycle.
- tmpfs mount = a third type, kept entirely in RAM. Volatile, fast, used for secrets and scratch.
- Volumes are portable across hosts (Docker recreates them on a fresh box from your compose file). Bind mounts depend on the exact host filesystem layout.
- Use volumes for persistent application state (DB data, uploads). Use bind mounts for dev workflows (live source mounting), config injection, and special filesystems.
Quick example
# Volume: Docker manages where it lives
$ docker run -d --name db1 \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# Storage: /var/lib/docker/volumes/pgdata/_data/ (managed by Docker)
# Bind mount: you point at an exact host path
$ docker run -d --name dev1 \
-v /home/me/projects/api/src:/app/src \
node:22 npm run dev
# Storage: /home/me/projects/api/src/ (your code, edited in your IDE)Same -v flag, two very different couplings.
Comparison table
| Aspect | Named volume | Bind mount |
|---|---|---|
| Source identifier | logical name (pgdata) | absolute host path (/home/me/x) |
| Storage location | /var/lib/docker/volumes/... (Docker-managed) | wherever you point |
| Created by | docker volume create or first docker run -v | Docker just mounts; you create dirs yourself |
| Lifecycle | Docker (volume rm, volume prune) | host (rm -rf, mv) |
| Portability across hosts | High — recreate on fresh host from compose | Low — depends on host's exact layout |
| Speed on Linux | Native filesystem speed | Native filesystem speed |
| Speed on Mac/Win | Native (lives in VM) | Slow (cross-VM-boundary syncing) |
| Initial copy from image | Yes (image content copied to empty volume on first mount) | No (mount is just a remap; image content gets shadowed) |
| Permissions handling | Owned by container; Docker manages UID/GID | Inherits host permissions; UID/GID mismatches common |
| Use case fit | Production state, declarative ops | Live-reload dev, config injection, host-side editing |
How they behave on "first mount with content"
This is the subtlest difference.
FROM nginx:1.27-alpine
# Image already has /usr/share/nginx/html/index.html# Volume mount: Docker copies the image's existing files into the empty volume
$ docker run -d -v webroot:/usr/share/nginx/html nginx:1.27-alpine
$ docker run --rm -v webroot:/data alpine ls /data
index.html # ← image's default file is now in the volume
# Bind mount: just a remap — image content is shadowed
$ mkdir /tmp/webroot # empty
$ docker run -d -v /tmp/webroot:/usr/share/nginx/html nginx:1.27-alpine
$ docker exec <id> ls /usr/share/nginx/html
# (empty — image's index.html is hidden by the empty bind mount)Volume mount: friendly first-time copy. Bind mount: literal mount-over.
When to pick volume
- Database state in production (Postgres, Mongo, Redis with AOF)
- User uploads in a web app
- Anything that should survive
docker rmand be portable to a new host - Stateful workloads in CI where you want predictable cleanup with
docker volume rm
When to pick bind mount
- Local dev with live source code: mount
./srcinto the container so file changes on your laptop reflect inside the running app immediately. The wholedocker compose -f compose.dev.yaml upstory. - Injecting config files: mount a single file (
./nginx.conf:/etc/nginx/nginx.conf:ro) into a container without rebuilding the image. - Sharing a secret file at runtime in a way that lets you rotate without rebuild.
- Special filesystems: an NFS mount, a fast NVMe device, a network share — bind-mount the host path that already has it.
Common mistakes
Bind-mounting a non-existent host path
$ docker run -v /tmp/does-not-exist:/data alpine ls /data
# (empty — Docker auto-created /tmp/does-not-exist on host as root-owned)Docker creates missing host paths automatically (as root). You end up with a stray empty directory on the host that you may or may not have wanted.
UID mismatch with bind mounts
$ ls -ld /home/me/data
drwx------ 2 me me 4096 ... # owned by host UID 1000
$ docker run -v /home/me/data:/data alpine touch /data/x
File created, but with UID=root inside container.
# On host: /home/me/data/x is owned by UID 0, unreadable by `me`.The container runs as root by default, writes as UID 0. The host sees those files as root-owned. Fix with --user $(id -u):$(id -g) or with explicit chown in the Dockerfile.
Slow Mac/Windows builds because of bind mount sync
Docker Desktop on macOS/Windows runs Linux in a VM. Bind mounts cross the host↔VM boundary, which is slow for many small files (node_modules, anyone?). Workarounds: use named volumes for node_modules even in dev, or use the :cached/:delegated consistency modes, or use a remote dev container.
Using a bind mount where a volume would do
# If you do not need to read/write from the host, use a volume:
$ docker run -v /opt/myapp/data:/data myapp # bind
$ docker run -v myapp_data:/data myapp # volume — no host couplingIf the host path is just storage you never touch directly, a volume is cleaner: portable, declarative, no permissions surprises.
Real-world usage
- Production: named volumes for databases. Almost universal.
- Local dev: bind mounts for source code (live reload), config files; volumes for persistent service state (DB, cache).
- CI runners: bind mounts of the workspace into build containers (
-v $PWD:/work); ephemeral cleanup withdocker rmplus--rmon run. - Configuration injection: read-only bind mounts of single config files (
-v ./prometheus.yml:/etc/prometheus/prometheus.yml:ro) — you can edit the file on host and restart the container.
Follow-up questions
Q: Can I switch from a bind mount to a volume without losing data?
A: Yes. Stop the container. docker run --rm -v old-bind-host-path:/from -v newvol:/to alpine cp -a /from/. /to/. Then start the new container with -v newvol:/data. The volume now holds what the bind mount had.
Q: Are volumes faster than bind mounts?
A: On Linux, no — both go through the same filesystem. On macOS/Windows, volumes are noticeably faster because they live inside the Linux VM and avoid the cross-boundary sync that bind mounts pay for.
Q: How do I back up a Docker volume?
A: Spin up a temporary container with the volume + a tar bind-mounted out:
docker run --rm -v pgdata:/data -v $PWD:/backup alpine \
tar czf /backup/pgdata.tar.gz -C /data .For bind mounts, just tar the host path directly — no Docker indirection needed.
Q: Can I read-only mount?
A: Yes — append :ro. Works for both forms: -v pgdata:/data:ro or -v ./conf:/etc/conf:ro. The container cannot write to that path. Useful for config files and shared read-only datasets.
Q: (Senior) When does the volume vs bind-mount choice actually affect production architecture?
A: When you move to multi-host orchestrators (Swarm, Kubernetes). Bind mounts presume a specific host filesystem — they tie the workload to one node. Named volumes can be backed by network storage drivers (NFS, EBS, Ceph) that survive node loss. Designing for portable volumes early saves a rewrite when you scale beyond one host.
Examples
Dev compose with bind mounts for code, volumes for state
# compose.dev.yaml
services:
api:
image: node:22-alpine
working_dir: /app
volumes:
- ./api/src:/app/src # bind mount: live code
- ./api/package.json:/app/package.json:ro
- api_node_modules:/app/node_modules # named volume: dep cache
command: npm run dev
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
volumes:
- pgdata:/var/lib/postgresql/data # named volume: persistent state
volumes:
api_node_modules:
pgdata:This is a common dev setup: code is bind-mounted (you edit on host, container picks up changes instantly), node_modules is a named volume (avoids slow Mac/Win cross-boundary sync), DB data is a named volume (clean lifecycle).
Read-only config injection
$ docker run -d \
-v ./prometheus.yml:/etc/prometheus/prometheus.yml:ro \
-v promdata:/prometheus \
-p 9090:9090 \
prom/prometheusConfig file: bind-mount, read-only, edited on host. Time-series data: named volume, persistent. Two storage types in one container, each chosen for what it is good at.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet