Bind Mounts vs Volumes in Docker: Which One Should You Use
by Eric Hanson, Backend Developer at Clean Systems Consulting
The wrong choice that compounds over time
Your team uses bind mounts for everything: source code, database data, config files, log output. It works locally. Then a developer joins from Windows, and the database volume has permission errors. CI runs slower because bind mount performance on Docker Desktop is half of native. The database volume ends up in the project directory, gets accidentally committed, or worse — gets deleted when someone cleans up the repo.
The choice between bind mounts and named volumes isn't about preference — they're designed for different use cases. Using one where the other belongs creates problems that are annoying to diagnose because the symptom (permissions error, slow build, missing data) doesn't obviously point to the storage type as the cause.
What each one actually is
Bind mount: A specific path on the host filesystem is mounted into the container. You control where on the host the data lives.
docker run -v /absolute/host/path:/container/path image
# or relative in Compose:
# - ./relative/path:/container/path
Named volume: Docker creates and manages a storage area. You refer to it by name. Docker controls where on the host it actually lives.
docker run -v my-volume-name:/container/path image
Both appear inside the container the same way — as a mounted directory. The differences are operational.
Use bind mounts for: source code and config files you edit
Bind mounts are the right tool when you want changes on the host to immediately appear inside the container. The canonical use case is development source code:
services:
app:
build: .
volumes:
- ./src:/app/src # changes here appear in the running container
command: npm run dev # file watcher picks up changes
Without the bind mount, you'd rebuild the image on every source change. With it, the file watcher inside the container sees edits in real time.
Config files you need to tune frequently also fit here:
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
:ro (read-only) prevents the container from modifying the config file, which is appropriate when the config is source-controlled and should only be changed intentionally.
When bind mounts make sense:
- Development source code (live reload)
- Config files managed in the repository
- Files you need to access from both host and container regularly
Use named volumes for: persistent application data
Named volumes are the right tool for data that needs to survive container lifecycle events but doesn't need to be directly managed by the user on the host filesystem.
services:
db:
image: postgres:16-alpine
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
pg_data:
Why named volumes for database data:
- Docker manages creation: no need to create a directory with the right permissions before running
- Permissions are correct: Docker initializes the volume, PostgreSQL initializes the data directory, no UID/GID mismatch
- Not exposed to the host: nobody accidentally deletes it, commits it, or runs disk cleanup scripts that remove it
- Performance: on Docker Desktop (Mac/Windows), named volumes use the VM's local filesystem directly, which is significantly faster than bind mounts that must cross the host-VM boundary
The performance difference is meaningful: on an M2 Mac with Docker Desktop, a PostgreSQL container with a bind mount to the host runs at roughly 20–40% of the speed of the same container with a named volume. For database-heavy integration tests, this is noticeable.
When named volumes make sense:
- Database storage (PostgreSQL, MySQL, MongoDB)
- Application state that persists across container recreations
- Cache directories that should survive restarts but don't need host-side access
- Any data that Docker should manage rather than the user
The node_modules pattern: named volume for a specific path
A common hybrid pattern: bind mount the entire project, but use a named volume for node_modules to prevent the host's node_modules from shadowing the container's:
services:
app:
build: .
volumes:
- .:/app # bind mount — source code on host
- node_modules:/app/node_modules # named volume — container-managed
Docker evaluates more specific mount paths last, so the named volume at /app/node_modules takes precedence over the bind mount at /app for that path. The container uses its own npm-installed node_modules, while the host's version is ignored.
Without the named volume for node_modules, the host's version (which may be absent, may have been installed for the wrong OS, or may be a different version) is mounted over the container's. The app fails with missing module errors, or with errors about binaries compiled for the wrong platform.
Permission problems: bind mounts
The most common bind mount problem: the host path is owned by uid:gid X, but the container process runs as uid:gid Y.
On Linux, UIDs are universal — if your host user is UID 1000 and the container process is UID 1001, the container process can't write to files owned by UID 1000. On Docker Desktop (Mac/Windows), the VM layer handles UID mapping and this is less commonly an issue.
Diagnosis:
# Check host directory ownership
ls -la ./your-bind-mount-path
# Check what UID the container process runs as
docker exec -it container-name id
Fix options:
- Set the container's user to match the host UID:
--user $(id -u):$(id -g) - Change the host directory ownership:
chown -R 1001:1001 ./your-path - Use a named volume instead, and copy data in/out as needed
For development bind mounts, option 1 (run container as the host user) is simplest:
services:
app:
user: "${UID}:${GID}" # from shell environment
Tmpfs: the third option nobody mentions
For data that needs to be writable but doesn't need to survive even a container restart:
services:
app:
tmpfs:
- /tmp
- /app/cache
tmpfs mounts are in-memory, fast, and automatically cleared when the container stops. Use them for:
- Temp files that applications write and immediately read
- Test databases when you want a truly fresh state each run
- Any path your app writes to but doesn't need persisted
In a Kubernetes pod:
volumes:
- name: tmp
emptyDir:
medium: Memory
The decision rule
| Use case | Tool |
|---|---|
| Source code during development | Bind mount |
| Config files from the repo | Bind mount (:ro) |
| Database data | Named volume |
| Application state | Named volume |
| node_modules or similar build artifacts | Named volume (specific path) |
| Temp files, caches that reset | tmpfs |
| Data you need to access from host tooling | Bind mount (with UID consideration) |
If you're using bind mounts for database storage, switch to named volumes. If you're using named volumes for development source code and wondering why live reload doesn't work, switch to bind mounts. The distinction maps cleanly to what each mechanism is designed to do.