Volumes vs Bind Mounts
Docker offers two primary ways to persist data outside the container's ephemeral filesystem: named volumes (managed by Docker) and bind mounts (direct host paths). Choosing the right one depends on whether you are storing data (volume) or syncing code (bind mount).
Visual Comparison
flowchart TD
subgraph Host["Docker Host"]
subgraph DockerManaged["/var/lib/docker/volumes/"]
Volume["Named Volume<br/>pgdata"]
end
subgraph UserPath["/home/user/myapp/"]
Bind["Bind Mount<br/>./src"]
end
end
subgraph Container
MP1["/var/lib/postgresql/data"]
MP2["/app/src"]
end
Volume -->|"docker volume create"| MP1
Bind -->|"Direct path mapping"| MP2
style Volume fill:#e8f5e9,stroke:#2e7d32
style Bind fill:#e3f2fd,stroke:#1565c0
Side-by-Side Comparison
| Feature | Named Volume | Bind Mount |
|---|---|---|
| Managed by | Docker (docker volume commands) | You (any host path) |
| Stored at | /var/lib/docker/volumes/... | Anywhere you specify |
| Portable | Yes -- easy to backup, restore, migrate | No -- depends on host path existing |
| Edit from host | Hard (need root or volume inspection) | Easy -- edit with your IDE |
| Best for | Databases, persistent app data | Source code during development |
| Permissions | Docker handles ownership | UID/GID mismatches common |
| Syntax | -v pgdata:/data (name, no path) | -v ./src:/app (path starts with . or /) |
Named Volumes (Production Default)
Docker manages the storage location. You interact with volumes by name, not by path:
# Create a volume
docker volume create pgdata
# Use it with a container
docker run -d \
--name db \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# The data survives even if the container is removed:
docker rm -f db
docker volume ls # pgdata still exists
Volume Management Commands
# List all volumes
docker volume ls
# Inspect a volume (see where it lives on disk)
docker volume inspect pgdata
# Remove a volume (data is permanently deleted!)
docker volume rm pgdata
# Remove all unused volumes (careful!)
docker volume prune
In Docker Compose
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: {} # Declares a named volume
Bind Mounts (Development Standard)
Map a specific directory on your host directly into the container. Changes are reflected immediately in both directions:
docker run -d \
--name app \
-v $(pwd)/src:/app/src \
node:20-alpine
Edit files in ./src on your host, and the container sees the changes instantly -- perfect for development with hot reloading.
In Docker Compose
services:
app:
image: node:20-alpine
working_dir: /app
volumes:
- ./:/app # Bind mount: current directory → /app
- node_modules:/app/node_modules # Named volume for dependencies
volumes:
node_modules: {}
Read-Only Bind Mounts
For config files that should not be modified by the container:
docker run -d \
-v /srv/app/config/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:alpine
The :ro flag makes the mount read-only inside the container.
Tmpfs Mounts (In-Memory)
For temporary data that should never touch disk (secrets, caches):
docker run -d --tmpfs /tmp:size=100M my-app
Tmpfs data lives in RAM and is lost when the container stops. Useful for sensitive data that should not persist.
Common Patterns
Database with Named Volume
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata: {}
Development Stack with Bind Mount + Volume
services:
app:
image: node:20-alpine
working_dir: /app
command: npm run dev
volumes:
- ./:/app # Code: bind mount for live editing
- node_modules:/app/node_modules # Dependencies: volume (faster, no OS conflicts)
ports:
- "3000:3000"
volumes:
node_modules: {}
Checking Mounts on a Running Container
# See what is mounted and where
docker inspect -f '{{json .Mounts}}' my-container | python3 -m json.tool
This shows the mount type (volume or bind), source path, destination path, and read/write mode.
Permission Issues
The most common problem with both mount types is UID/GID mismatch -- the container runs as one user, but the host files are owned by a different user:
| Symptom | Cause | Fix |
|---|---|---|
Permission denied in container | Container user cannot read/write host files | Match UID/GID or use chown |
Files created by container owned by root on host | Container runs as root, host expects different owner | Run container as non-root user (--user) |
Decision Guide
| Scenario | Use | Why |
|---|---|---|
| PostgreSQL/MySQL data directory | Named volume | Portable, easy to backup, Docker manages it |
| Redis persistence | Named volume | Same as databases |
| Application source code (dev) | Bind mount | Live editing from host IDE |
| Config file injection | Bind mount (:ro) | Host manages config, container reads it |
| Temporary caches / secrets | Tmpfs | Never persisted to disk |
| CMS media uploads | Named volume | Survives container updates |
Key Takeaways
- Named volumes are the production default for any data that must survive container recreation (databases, uploads, state).
- Bind mounts are the development default for source code -- they enable live editing and hot reloading.
- Docker manages named volumes at
/var/lib/docker/volumes/. You interact with them by name, not path. - Never store important data in the container's filesystem without a mount -- it will be lost on
docker rm. - Use
:rofor bind mounts that the container should not modify. - Watch for UID/GID permission mismatches -- the most common mount issue.
What's Next
- Continue to Backup and Restore to learn how to back up and migrate volume data.