Volumes & Storage
Solving the persistence problem — bind mounts, named volumes, and tmpfs
The Persistence Problem
Why Container Storage Is Not Enough
- Container's read-write layer (overlay2 upperdir) is ephemeral — destroyed when the container is removed
- Overlay2 copy-on-write adds overhead for write-heavy workloads: first write to a file copies the entire file from lowerdir to upperdir
- Two problems:
- Data loss — database files, uploads, state all vanish with
docker rm - Performance — CoW overhead is unacceptable for databases, logging, I/O-intensive apps
- Data loss — database files, uploads, state all vanish with
- Solution: mount host storage directly into the container, bypassing the overlay filesystem entirely
Three Mount Types
Bind Mounts
- Map a host directory/file directly into the container
- Kernel
bind mount— same underlying filesystem, direct access - Changes are bidirectional and immediate
- Container sees host files; host sees container writes
# -v syntax
docker run -v /host/path:/container/path
# --mount syntax (preferred)
docker run --mount \
type=bind,\
source=/host/path,\
target=/container/path
# Read-only bind mount
docker run --mount \
type=bind,\
source=/host/path,\
target=/container/path,\
readonlyGood for: development (live code reload), sharing config files
Risk: container can modify host files (unless readonly)
Named Volumes
- Managed by Docker, stored in
/var/lib/docker/volumes/<name>/_data - Docker manages lifecycle: create, list, inspect, remove
- Volume population: if the image has data at the mount point, Docker copies it into the volume on first use
- Volume drivers: local (default), NFS, AWS EBS, Azure Files
# -v syntax
docker run -v mydata:/container/path
# --mount syntax (preferred)
docker run --mount \
type=volume,\
source=mydata,\
target=/container/path
# Volume management
docker volume create mydata
docker volume ls
docker volume inspect mydata
docker volume rm mydataGood for: databases, persistent application data
tmpfs Mounts
- In-memory filesystem, never written to disk
- Data lost when container stops
- Faster than disk-backed storage
- Not shared between containers
# --tmpfs syntax
docker run --tmpfs /container/path
# --mount syntax (preferred)
docker run --mount \
type=tmpfs,\
target=/container/path
# With size limit
docker run --mount \
type=tmpfs,\
target=/run/secrets,\
tmpfs-size=64mGood for: secrets, temporary scratch space, sensitive data that shouldn't persist on disk
Note
Key difference: Bind mounts depend on the host's directory structure. Named volumes are fully
managed by Docker and portable. Always prefer --mount syntax over -v — it's more
explicit and catches errors (e.g., -v silently creates a directory if the source doesn't exist;
--mount fails with an error for bind mounts).
How Mounts Bypass Overlay
Volumes and bind mounts are overlaid on top of the overlay2 merged view at specific mount points. Writes to those paths go directly to the backing storage, completely bypassing the copy-on-write mechanism.
/usr, /bin, /lib, ...
Read from image layers (lowerdir)
Write via CoW to upperdir
/var/lib/mysql
Named volume
Direct I/O — bypasses overlay
/app/src
Bind mount
Direct I/O — bypasses overlay
Write to /usr/bin/foo
Goes through overlay2 CoW
Write to /var/lib/mysql/data
Direct to volume — no CoW overhead
Write to /app/src/main.py
Direct to host directory — visible immediately
Storage Drivers vs Volumes
| Aspect | Storage Driver (overlay2) | Volumes |
|---|---|---|
| Purpose | Container filesystem (image layers + RW layer) | Persistent data |
| Performance | Copy-on-write overhead on first write | Direct I/O — native filesystem performance |
| Lifecycle | Dies with the container (docker rm) |
Independent of container lifecycle |
| Sharing | Not shareable between containers | Can be shared (mounted into multiple containers) |
| Backup | Complex — data is in overlay layer format | Simple — regular directory on the host filesystem |
| Portability | Tied to image build | Independent, can be backed by NFS/cloud drivers |
Volume Population — Bind Mounts vs Named Volumes
If the image has files at /usr/share/nginx/html:
docker run -v mysite:/usr/share/nginx/html nginxDocker copies image files into the volume on first mount (volume is empty). Subsequent mounts use the volume's existing data.
Image data is preserved.
If the image has files at /usr/share/nginx/html:
docker run -v /my/html:/usr/share/nginx/html nginxThe host directory completely obscures the image contents.
If /my/html is empty, the container sees an empty directory.
Image data is hidden.
Best Practices
Databases
Always use named volumes. Never store database data in the container layer.
docker run -d \
--name postgres \
-v pgdata:/var/lib/postgresql/data \
postgres:16Config Files
Use bind mounts with :ro (read-only) to inject configuration.
docker run -d \
--name nginx \
--mount type=bind,\
source=./nginx.conf,\
target=/etc/nginx/nginx.conf,\
readonly \
nginxSecrets / Sensitive Data
Use tmpfs mounts so secrets never touch disk. In Swarm mode, use Docker secrets.
docker run -d \
--mount type=tmpfs,\
target=/run/secrets,\
tmpfs-size=1m \
myappDevelopment
Use bind mounts for live code reload — edit on host, see changes immediately in container.
docker run -d \
-v $(pwd)/src:/app/src \
-p 3000:3000 \
node-devWarning
Warning: Don't mount /var/run/docker.sock into a container unless absolutely necessary.
This gives the container full control over Docker and effectively root access to the host.
See Docker Architecture — Socket File Security.
Volume Commands — Quick Reference
$ docker volume create mydata
mydata
$ docker volume ls
DRIVER VOLUME NAME
local mydata
local pgdata
$ docker volume inspect mydata
[{
"CreatedAt": "2026-03-31T10:00:00Z",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/mydata/_data",
"Name": "mydata",
"Scope": "local"
}]
$ docker volume rm mydata
mydata
$ docker volume prune
WARNING! This will remove anonymous local volumes not used by at least one container.
Total reclaimed space: 1.2GB
Tip
Tip: Use docker volume prune periodically to clean up orphaned anonymous volumes.
Named volumes are not removed by prune — only anonymous (unnamed) volumes that are no longer referenced by any container.