Skip to main content

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

FeatureNamed VolumeBind Mount
Managed byDocker (docker volume commands)You (any host path)
Stored at/var/lib/docker/volumes/...Anywhere you specify
PortableYes -- easy to backup, restore, migrateNo -- depends on host path existing
Edit from hostHard (need root or volume inspection)Easy -- edit with your IDE
Best forDatabases, persistent app dataSource code during development
PermissionsDocker handles ownershipUID/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:

SymptomCauseFix
Permission denied in containerContainer user cannot read/write host filesMatch UID/GID or use chown
Files created by container owned by root on hostContainer runs as root, host expects different ownerRun container as non-root user (--user)

Decision Guide

ScenarioUseWhy
PostgreSQL/MySQL data directoryNamed volumePortable, easy to backup, Docker manages it
Redis persistenceNamed volumeSame as databases
Application source code (dev)Bind mountLive editing from host IDE
Config file injectionBind mount (:ro)Host manages config, container reads it
Temporary caches / secretsTmpfsNever persisted to disk
CMS media uploadsNamed volumeSurvives 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 :ro for bind mounts that the container should not modify.
  • Watch for UID/GID permission mismatches -- the most common mount issue.

What's Next