Suggest an editImprove this articleRefine the answer for “Docker volume vs bind mount: what is the difference?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**A volume** is Docker-managed storage with a logical name; **a bind mount** is a direct mapping of an arbitrary host path into the container. Volumes are portable and managed; bind mounts couple you to a specific host filesystem path. ```bash docker run -v mydata:/app/data myimg # named volume (Docker manages it) docker run -v /home/me/code:/app/code myimg # bind mount (this exact host path) ``` **Key:** named volumes for production state (databases, uploads) — Docker portable, declarative, backups/migration easier. Bind mounts for dev workflows where you edit on host and reload in container, and for cases that need a specific host path.Shown above the full answer for quick recall.Answer (EN)Image**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 ```bash # 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. ```dockerfile FROM nginx:1.27-alpine # Image already has /usr/share/nginx/html/index.html ``` ```bash # 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 rm` and 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 `./src` into the container so file changes on your laptop reflect inside the running app immediately. The whole `docker compose -f compose.dev.yaml up` story. - **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** ```bash $ 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** ```bash $ 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** ```bash # 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 coupling ``` If 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 with `docker rm` plus `--rm` on 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: ```bash 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 ```yaml # 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 ```bash $ docker run -d \ -v ./prometheus.yml:/etc/prometheus/prometheus.yml:ro \ -v promdata:/prometheus \ -p 9090:9090 \ prom/prometheus ``` Config 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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.