Skip to main content

Volumes and Bind Mounts

By default, every file a container writes goes into its writable layer — a thin, container-specific filesystem that is thrown away the moment you remove the container. For stateless workloads (web servers, API handlers) that is fine. For anything that holds state — databases, uploaded files, log aggregators — you need a way to survive container restarts and replacements.

Docker solves this with two mechanisms: named volumes and bind mounts. They look similar from the outside (both use the -v flag) but serve different purposes and have different ownership semantics.


The Three Mount Types

Mount typeManaged byStorage locationBest for
Named volumeDocker/var/lib/docker/volumes/Database data, persistent app state
Bind mountYouAnywhere on the hostSource code during dev, config files
tmpfs mountKernelHost RAM (not persisted)Sensitive data, non-persistent scratch space

Named Volumes

A named volume is a directory that Docker creates and manages on the host. You reference it by a logical name, and Docker handles the actual path.

Creating and listing volumes

# Create a volume explicitly
docker volume create pgdata

# List all volumes
docker volume ls

# Inspect a volume (see its mountpoint on the host)
docker volume inspect pgdata

docker volume inspect output:

[
{
"CreatedAt": "2024-11-15T09:23:44Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Name": "pgdata",
"Options": {},
"Scope": "local"
}
]

Using a named volume

# Mount a named volume at /var/lib/postgresql/data inside the container
docker run -d \
--name postgres-dev \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16

Stop and remove the container — the data in pgdata survives:

docker stop postgres-dev
docker rm postgres-dev

# Start a brand new container using the same volume — data is still there
docker run -d \
--name postgres-dev \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16

Auto-created volumes

You do not have to docker volume create explicitly. If you reference a volume name that does not exist, Docker creates it automatically:

# This creates 'myvolume' if it doesn't exist
docker run -v myvolume:/data busybox

Volume initialisation behaviour

When Docker mounts a named volume into a container and the volume is empty, Docker copies the existing contents of the container's directory into the volume. This is how official database images seed the volume with initial data — Postgres pre-populates /var/lib/postgresql/data on first run.

Bind mounts do not have this behaviour — they simply overlay whatever is on the host.


Bind Mounts

A bind mount maps a specific directory or file from the host filesystem into the container. Changes on either side are immediately visible on the other.

# Mount the current working directory into /app in the container
docker run -d \
--name dev-server \
-p 3000:3000 \
-v $(pwd):/app \
-w /app \
node:20-alpine \
node src/index.js

Now every file change you save in your editor is instantly visible inside the container. Pair this with a process watcher like nodemon and you get hot-reloading without rebuilding the image:

docker run -d \
--name dev-server \
-p 3000:3000 \
-v $(pwd):/app \
-w /app \
node:20-alpine \
npx nodemon src/index.js

Bind mount syntax variants

# Short form (host_path:container_path)
-v /absolute/host/path:/container/path

# Short form with options (read-only)
-v /host/config:/etc/myapp/config:ro

# Long form (--mount) — more explicit
--mount type=bind,source=/host/path,target=/container/path,readonly

The --mount form is verbose but removes ambiguity, which is useful in scripts and Compose files where clarity matters.


Read-Only Mounts

Mounting a volume or bind mount as read-only prevents the container from writing to it. This is a useful security measure for configuration files.

# Nginx reads config but cannot modify it
docker run -d \
-p 8080:80 \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx

Sharing Volumes Between Containers

Multiple containers can mount the same volume simultaneously. This is how sidecar patterns work — for example, a log shipper container reading log files written by the main app.

# Main app writes logs to the volume
docker run -d \
--name app \
-v shared-logs:/var/log/myapp \
my-app

# Log shipper reads from the same volume
docker run -d \
--name log-shipper \
-v shared-logs:/logs:ro \
fluentd

Be careful with concurrent writes from multiple containers — you are responsible for handling any write conflicts at the application level.


Removing Volumes

Volumes are not removed when you remove a container. This is intentional — Docker does not want to silently destroy your database data.

# Remove a specific volume (must not be in use by any container)
docker volume rm pgdata

# Remove all volumes not currently used by any container
docker volume prune

# Remove a container AND its anonymous volumes at the same time
docker rm -v my-container

Anonymous volumes — those created without a name, such as the implicit volume created by a VOLUME instruction in a Dockerfile — are removed with the container when you pass -v to docker rm.


tmpfs Mounts

A tmpfs mount lives in the host's RAM and is never written to disk. It disappears when the container stops. Use it for sensitive data (API keys, session tokens) that must not be persisted, or for high-speed temporary scratch space.

docker run -d \
--name my-app \
--mount type=tmpfs,destination=/tmp,tmpfs-size=100m \
my-app

Volume Drivers

The default local driver stores volumes on the host filesystem. Docker supports third-party volume drivers that store data elsewhere:

  • convoy — AWS EBS, NFS
  • rexray — Multi-cloud (AWS, GCE, Azure)
  • nfs — Network File System shares
# Create a volume using an NFS driver (plugin must be installed)
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.100,rw \
--opt device=:/data/shared \
nfs-data

For most developers, the local driver is all you need. Volume drivers become relevant when running Docker on multiple nodes (Swarm) or mounting cloud storage.


When to Use Each

Use named volumes when:

  • You are running a database (Postgres, MySQL, MongoDB, Redis)
  • You need data to survive container replacement in production
  • You do not care about the exact location of files on the host
  • You want Docker Compose to manage the lifecycle

Use bind mounts when:

  • You are developing locally and want your code changes to reflect immediately
  • You need to inject configuration files from the host (nginx.conf, .env)
  • You need to read host files from inside a container (e.g., SSH keys for a CI runner)
  • You want to write output to a known host path (reports, exports)

Never use bind mounts for:

  • Database data in production (host path dependencies make deployments fragile)
  • Secrets management (use Docker secrets or a vault instead)

Docker Compose Volumes

In Compose, volumes are declared at two levels: under the service (where to mount) and at the top level (volume definition).

services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data

app:
build: .
volumes:
# Bind mount for development hot-reloading
- ./src:/app/src
# Named volume for uploaded files
- uploads:/app/uploads

volumes:
pgdata:
uploads:

Running docker compose down does not remove named volumes by default. Pass --volumes to also remove them:

docker compose down --volumes

Inspecting Volume Data

Sometimes you need to peek at what is in a volume without starting the application container. The quickest way is to mount it into a utility container:

# Open a shell with the volume mounted
docker run --rm -it \
-v pgdata:/data \
alpine sh

# Inside the shell
ls /data

Or copy data out of a volume to the host:

# Copy from volume to current directory
docker run --rm \
-v pgdata:/source \
-v $(pwd):/dest \
alpine cp -r /source/. /dest/backup/

Summary

ConceptKey command
Create a volumedocker volume create myvolume
List volumesdocker volume ls
Inspect a volumedocker volume inspect myvolume
Mount named volume-v myvolume:/container/path
Mount bind (host dir)-v /host/path:/container/path
Read-only mount-v /host/path:/container/path:ro
Remove a volumedocker volume rm myvolume
Remove unused volumesdocker volume prune

Data persistence is one of the first things developers get wrong with Docker. Understanding the difference between named volumes (Docker-managed, portable) and bind mounts (host-coupled, convenient for development) will save you from unexpected data loss and help you design robust production setups.