Docker Volumes 2025 — Persistent Data Management for Containers
Advertisement
Introduction
Why This Matters
Containers are ephemeral by design. When a container is removed, everything written to its filesystem disappears. For stateless applications (APIs, workers) this is exactly what you want. But databases, file uploads, application logs, and configuration files need to persist across container restarts, updates, and replacements.
Docker volumes solve this. They decouple storage from container lifecycle, enabling you to upgrade a PostgreSQL container to a new version without losing your data. They also enable containers to share data with each other and with the host system during development.
Understanding volumes is also essential for Kubernetes, where PersistentVolumes and PersistentVolumeClaims follow the same conceptual model. Engineers who understand Docker volumes transition to Kubernetes storage much faster.
Three Storage Types
Docker provides three mechanisms for persisting data:
| Type | Stored | Managed By | Best For |
|---|---|---|---|
| Named Volume | Docker managed path | Docker | Database data, production persistence |
| Bind Mount | Any host path | User | Local dev, config files, hot reload |
| tmpfs Mount | Host RAM only | Kernel | Temporary data, secrets in memory |
Named Volumes
Named volumes are the recommended way to persist data in production. Docker manages the storage location, and volumes outlive the containers that use them.
# Create a named volume
docker volume create pgdata
# Use a named volume in a container
docker run -d \
--name postgres \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16-alpine
# Stop and remove the container — data persists in pgdata
docker rm -f postgres
# Restart with same or newer postgres version — data is still there
docker run -d \
--name postgres \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16.2-alpineVolume Commands
# List all volumes
docker volume ls
# Inspect a volume (shows mount point on host)
docker volume inspect pgdata
# "Mountpoint": "/var/lib/docker/volumes/pgdata/_data"
# Remove a specific volume (fails if in use by a container)
docker volume rm pgdata
# Remove all unused volumes (cleanup)
docker volume prune
# Remove all unused volumes without confirmation
docker volume prune -fBind Mounts
Bind mounts map a specific host filesystem path into a container. Changes in either direction are immediately visible.
# Mount current directory into container (development hot reload)
docker run -d \
-v $(pwd)/src:/app/src \
-v $(pwd)/config:/app/config:ro \
-p 3000:3000 \
myapp:dev
# The :ro suffix makes the mount read-only inside the containerBind mounts are ideal for development workflows where you want the container to pick up file changes without rebuilding the image. They are not recommended for production because they tie the container to a specific host path.
# Mount host nginx config for easy editing
docker run -d \
-p 80:80 \
-v /etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
-v /var/www/html:/usr/share/nginx/html:ro \
nginx:1.27-alpinetmpfs Mounts
tmpfs mounts store data in the host's RAM. Data is lost when the container stops. Use for sensitive temporary data (tokens, session state) or performance-sensitive scratch space.
# tmpfs mount via --tmpfs flag
docker run -d \
--tmpfs /tmp:rw,size=64m \
--tmpfs /var/run:rw \
myapp:1.0
# tmpfs via -v syntax
docker run -d \
--mount type=tmpfs,destination=/tmp,tmpfs-size=67108864 \
myapp:1.0Volumes in Docker Compose
version: '3.8'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
# Named volume for data persistence
- pgdata:/var/lib/postgresql/data
# Bind mount for initialization scripts
- ./db/init:/docker-entrypoint-initdb.d:ro
api:
build: .
volumes:
# Bind mount for hot reload in development
- ./src:/app/src
# Named volume to persist uploaded files
- uploads:/app/uploads
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
# Declare all named volumes used by services
volumes:
pgdata:
uploads:
redis_data:Sharing Volumes Between Containers
Multiple containers can mount the same volume simultaneously:
# Create a shared volume
docker volume create shared-logs
# Container 1: writes logs
docker run -d \
--name app \
-v shared-logs:/var/log/app \
myapp:1.0
# Container 2: reads and ships logs (e.g., Filebeat)
docker run -d \
--name log-shipper \
-v shared-logs:/var/log/app:ro \
elastic/filebeat:8.13.0In Docker Compose, use volumes_from:
services:
app:
image: myapp
volumes:
- app-logs:/var/log/app
log-shipper:
image: elastic/filebeat:8.13.0
volumes_from:
- appBackup and Restore Strategies
# Backup a named volume to a tar archive
docker run --rm \
-v pgdata:/source:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/pgdata-backup-$(date +%Y%m%d).tar.gz -C /source .
# Restore from backup
docker run --rm \
-v pgdata:/target \
-v $(pwd):/backup \
alpine \
tar xzf /backup/pgdata-backup-20250301.tar.gz -C /target
# PostgreSQL-specific: use pg_dump for logical backups
docker exec postgres pg_dump -U app mydb | gzip > db-backup.sql.gz
# Restore PostgreSQL
gunzip -c db-backup.sql.gz | docker exec -i postgres psql -U app mydbVolume Drivers for Production Storage
The default local volume driver stores data on the Docker host's disk. For distributed or cloud storage, use volume plugins:
# AWS EFS (via rexray/efs or efs-provisioner)
docker volume create \
--driver rexray/efs \
--name efs-data \
--opt size=20
# NFS volume
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=nfs-server.example.com,rw,nfsvers=4 \
--opt device=:/exports/data \
nfs-data
docker run -d -v nfs-data:/data myapp:1.0In production Kubernetes clusters, PersistentVolumeClaims handle this automatically using StorageClasses (EBS, EFS, GCE PD, Azure Disk).
Inspecting and Debugging Volumes
# Find which containers use a volume
docker ps -a --filter volume=pgdata
# Inspect volume contents without starting your app container
docker run --rm \
-v pgdata:/data \
alpine ls -la /data
# Check disk usage
docker system df -v
# Shows volumes and their disk usage
# Access PostgreSQL data files directly
docker run --rm \
-v pgdata:/data:ro \
alpine find /data -name "*.conf"Common Mistakes
- Deleting containers with
docker rm -vunintentionally removes associated anonymous volumes - Using bind mounts in production with absolute host paths — breaks portability across servers
- Not declaring volumes in
docker-compose.yml— Compose creates anonymous volumes that are hard to manage - Mounting the entire host filesystem (
-v /:/host) for debugging — extreme security risk in production - Using tmpfs for database storage — all data lost on container stop or restart
Best Practices
- Always use named volumes for database persistence, not anonymous volumes or bind mounts
- Use
:ro(read-only) for configuration file bind mounts to prevent container modifications - Back up named volumes before upgrading database container versions
- Use
docker system df -vregularly to audit volume disk usage and identify orphaned volumes - In production, use a volume plugin (EFS, NFS, Ceph) for data that must be accessible across multiple hosts
- Run
docker volume prunein CI pipelines after tests to reclaim disk space from ephemeral test volumes
Key Takeaways
- Named volumes are Docker-managed and persist independently of any container lifecycle — the recommended choice for production databases
- Bind mounts map host paths into containers and reflect changes immediately — ideal for development hot-reload workflows
- tmpfs mounts store data in RAM and are lost on container stop — use for session data, secrets, or scratch space
- Multiple containers can mount the same named volume simultaneously — enables sidecar log shippers and backup agents
docker volume pruneremoves all unused volumes — use carefully in production, aggressively in CI- Always declare named volumes explicitly in
docker-compose.yml; undeclared volumes become hard-to-track anonymous volumes - Use
pg_dumpfor logical PostgreSQL backups and volume tar archives for filesystem-level backups - In Kubernetes, named volumes map conceptually to PersistentVolumeClaims backed by StorageClasses
Advertisement