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​

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​