Volume #
In the container world, data is the most vulnerable thing to lose. Containers are ephemeral: when a container is deleted, its entire filesystem disappears with it. Without a persistent storage mechanism, Docker is only good for experiments — not for real applications.
This is where Docker Volumes come in. Volumes are Docker’s official mechanism for storing and managing data persistently, separate from the container lifecycle. They’re the foundation of every production storage strategy in Docker.
This article covers volumes in depth: from the basic concept, the types, how they work, CLI and Compose syntax, to integration with external storage via volume drivers. After reading this, you’ll have a solid understanding for choosing and implementing volumes in every situation.
What Is a Docker Volume? #
A Docker Volume is a special directory that:
- Is stored outside the container filesystem (on the host, in a Docker-managed location).
- Is fully managed by the Docker Engine — not by the user or the OS.
- Is used for persistent data storage — surviving container deletion, restarts, and recreation.
Technically, volumes are stored on the host in a Docker-managed directory (default /var/lib/docker/volumes/). But the container doesn’t need to know the physical location — Docker manages the mapping between the container path and the host path.
flowchart LR
subgraph HOST["Host Machine"]
H1[Container A<br/>/app/data]
H2[Container B<br/>/var/lib/mysql]
H3[Container C<br/>/uploads]
VOL[Docker Volume<br/>app-data<br/>stored in /var/lib/docker/volumes/]
end
H1 --> VOL
H2 --> VOL
H3 --> VOL
style VOL fill:#ffd8a8Volumes are designed for data that must survive:
- Databases (MySQL, PostgreSQL, MongoDB, Redis).
- File uploads (images, documents, attachments).
- Caches that are expensive to rebuild.
- Shared data between containers.
- Configuration that must persist (though bind mounts usually fit this better).
Why Docker Volumes Are Needed #
Without volumes, Docker is nearly useless for production applications. The reason is simple.
flowchart TB
subgraph WITHOUT["Without Volumes"]
A1[App Container] --> A2[Data in /app/data]
A2 --> A3[Container deleted]
A3 --> A4[Data LOST]
end
subgraph WITH["With Volumes"]
B1[App Container] --> B2[Data in /app/data]
B2 --> B3[Volume mount]
B3 --> B4[Container deleted]
B4 --> B5[Volume remains]
B5 --> B6[New container can mount the same volume]
end
style A4 fill:#ff6b6b,color:#fff
style B5 fill:#51cf66,color:#fff
style B6 fill:#51cf66,color:#fffFive problems volumes solve:
- Persistence — data survives outside the container lifecycle.
- Backup — data can be backed up easily (volume = a host directory).
- Sharing — many containers can mount the same volume.
- Migration — data can be moved to another host (volume = a host folder).
- Decoupling — data is separated from the application, per the twelve-factor app principle.
The important principle to hold onto:
Containers are stateless; data is stateful and lives outside the container.
This isn’t a style choice — it’s how Docker works. A container storing state in its internal filesystem is an anti-pattern.
Twelve-Factor App, Factor VI: “Execute the app as one or more stateless processes.” This means the application must not store state in its process. State must live in a backing service — which in the Docker context is a volume, an external database, or object storage.
Storage Types in Docker — Comparison #
Docker provides three storage mechanisms. Each has different characteristics.
| Mechanism | Location | Persistent | Managed By | Use Case |
|---|---|---|---|---|
| Volume | /var/lib/docker/volumes/ | Yes | Docker | Production, databases, multi-container |
| Bind Mount | Manual host path | Yes | User / OS | Development, live reload, config |
| tmpfs | RAM | No | OS | Caches, ephemeral secrets |
flowchart TB
P[Storage in Docker] --> V[Volume]
P --> B[Bind Mount]
P --> T[tmpfs]
V --> V1[✓ Production]
V --> V2[✓ Databases]
V --> V3[✓ Multi-container]
V --> V4[✓ Portable]
B --> B1[✓ Development]
B --> B2[✓ Live reload]
B --> B3[✓ Config files]
B --> B4[✗ Not portable]
T --> T1[✓ Fast]
T --> T2[✓ Sensitive]
T --> T3[✗ Not persistent]Volumes are the default production choice. Managed by Docker, portable, safe, and support volume drivers for external storage integration.
Bind mounts fit development and configuration file access. Not portable because they depend on the host structure.
tmpfs for data that needs very fast access and doesn’t need to persist (caches, sessions, secrets).
Full details on bind mounts and tmpfs are covered in their own articles. This article focuses on volumes.
How Docker Volumes Work #
To truly understand volumes, you need to know what happens behind the scenes when you create and use them.
Volume Anatomy #
flowchart TB
subgraph DOCKER["Docker Host"]
subgraph VOLDIR["/var/lib/docker/volumes/"]
V1[mysql-data/<br/>_data/ ← volume contents]
V2[app-data/<br/>_data/]
V3[cache-data/<br/>_data/]
end
subgraph MOUNT["Mount Points in Containers"]
M1[/var/lib/mysql/]
M2[/app/data/]
M3[/tmp/cache/]
end
end
V1 -.->|bind mount| M1
V2 -.->|bind mount| M2
V3 -.->|bind mount| M3
style V1 fill:#a8d8a8
style V2 fill:#a8d8a8
style V3 fill:#a8d8a8Each volume is a directory on the host. When a container mounts a volume, Docker creates a bind mount (internal, kernel-level) between the volume directory on the host and the path in the container. The container only sees its mount point — it doesn’t know or care where the data lives on the host.
Volume Stages #
stateDiagram-v2
[*] --> Created: docker volume create
Created --> Mounted: container mounts volume
Mounted --> Active: container running, reads/writes data
Active --> Unmounted: container stops/removed
Unmounted --> Mounted: new container mounts the same volume
Unmounted --> Removed: docker volume rm
Removed --> [*]The volume lifecycle is independent of the container. Containers can be created and deleted many times, and the same volume remains.
What Happens During docker volume create
#
docker volume create mysql-data
Docker creates the directory:
/var/lib/docker/volumes/mysql-data/
├── _data/ # volume contents, empty when freshly created
└── _metadata/ # Docker metadata (name, driver, options, etc.)
The _data directory is where the application data will be written. The Docker daemon manages its ownership and permissions.
Volume Syntax in the Docker CLI #
Creating Volumes #
# Create a named volume
docker volume create mysql-data
# Create a volume with a specific driver
docker volume create --driver local --opt type=nfs mysql-data
# Create a volume with a label
docker volume create --label env=production mysql-data
Listing Volumes #
# All volumes
docker volume ls
# Filter unused (dangling) volumes
docker volume ls -f dangling=true
# Format output
docker volume ls --format "{{.Name}}: {{.Driver}}"
Inspecting Volumes #
docker volume inspect mysql-data
Output:
[
{
"CreatedAt": "2026-06-06T12:00:00Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
"Name": "mysql-data",
"Options": {},
"Scope": "local"
}
]
Removing Volumes #
# Remove one volume
docker volume rm mysql-data
# Remove all unused volumes
docker volume prune
# Remove with confirmation
docker volume prune --force
Warning:docker volume rmanddocker volume prunepermanently delete data. There’s no recycle bin, no undo. Always back up before deleting a volume containing important data.
Mounting Volumes to Containers #
The -v format:
docker run -d \
--name mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8
The --mount format (more explicit):
docker run -d \
--name mysql \
--mount source=mysql-data,target=/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8
The format differences:
-v— more concise, short syntax.--mount— clearer, every component written explicitly, easier to script.
Named Volumes vs Anonymous Volumes #
Named volumes (recommended):
# Volume created with an explicit name
docker volume create app-data
docker run -v app-data:/app/data my-app
Advantages: easy to identify, back up, and manage.
Anonymous volumes (not recommended):
# Docker auto-generates a random name
docker run -v /app/data my-app
# Volume named "abc123def456..." (random hash)
Drawbacks: hard to track, prone to becoming dangling volumes, hard to back up.
Best practice: Always use named volumes in production. Anonymous volumes tend to pile up as “dangling volumes” and are hard to manage. If you ever run docker volume ls and see many volumes with random hash names, that’s a sign of anonymous volumes that need pruning.Volumes in Docker Compose #
Docker Compose is the most common way to define volumes in modern environments. It’s declarative, reproducible, and self-documenting.
Basic Syntax #
version: "3.9"
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
On the first docker compose up, Docker automatically creates the pgdata volume if it doesn’t exist. On docker compose down, containers are removed but the volume remains.
Volume Options #
volumes:
pgdata:
driver: local
driver_opts:
type: none
o: bind
device: /path/on/host
encrypted-data:
driver: local
driver_opts:
type: btrfs
device: /dev/sdb1
With driver_opts, you can specify:
- Filesystem type (btrfs, tmpfs, nfs, etc.).
- Device (host path or device path).
- Mount options (rw, ro, etc.).
Removing Volumes on down
#
# Default: volumes remain
docker compose down
# Remove containers AND volumes
docker compose down -v
Warning: down -v removes all volumes defined in the compose file. For production data, you almost always want not to remove volumes.
Sharing Volumes Between Services #
services:
app:
image: my-app
volumes:
- shared-data:/app/data
worker:
image: my-worker
volumes:
- shared-data:/input
volumes:
shared-data:
The app and worker services mount the same volume. Good for the “app produces files, worker processes files” pattern.
Sharing Volumes Between Containers #
One of volumes’ strengths is being shareable with many containers simultaneously. This is an important pattern in many architectures.
Pattern 1: Web Server + Application Server #
services:
nginx:
image: nginx
volumes:
- app-data:/var/www/html:ro # read-only
app:
build: ./app
volumes:
- app-data:/app/public
volumes:
app-data:
app writes static files (compiled assets, generated PDFs) to the volume. nginx reads the same files with read-only permission. nginx can’t corrupt the data; the app can.
Pattern 2: Producer + Consumer #
services:
producer:
build: ./producer
volumes:
- queue-data:/queue
consumer:
build: ./consumer
volumes:
- queue-data:/jobs
volumes:
queue-data:
producer writes jobs to the volume. consumer reads jobs and processes them. Simple decoupling without a message broker.
Pattern 3: App + Backup Service #
services:
app:
image: my-app
volumes:
- app-data:/app/data
backup:
image: alpine
volumes:
- app-data:/source:ro
- ./backups:/backup
command: tar czf /backup/app-$(date +%F).tar.gz -C /source .
volumes:
app-data:
The backup service mounts the same volume as app, but in read-only mode. Backups can run manually or via cron, and the backup container is ephemeral.
flowchart LR
A[App Service<br/>write] --> VOL[app-data]
VOL --> B[Backup Service<br/>read-only]
VOL --> C[Analyze Service<br/>read-only]
VOL --> D[Migrate Service<br/>read-write]
style A fill:#a8d8a8
style B fill:#a8d8a8
style C fill:#a8d8a8
style D fill:#ffd8a8Important pattern: Different services may mount the same volume with different access modes. Producer services need read-write; consumer services can be read-only. This is good isolation — a consumer can’t corrupt data even if its container is compromised.
Volume Drivers — Integration with External Storage #
Docker supports volume drivers for integrating volumes with external storage. This matters for:
- Production — more reliable storage than a local disk.
- High availability — data replicated across multiple zones/regions.
- Multi-host — volumes mountable from many hosts simultaneously.
Available Drivers #
| Driver | Storage | Use Case |
|---|---|---|
local | Local filesystem (default) | Single-host deployment |
nfs | NFS share | Multi-host Linux |
amazon/efs | AWS EFS | AWS, multi-AZ |
amazon/ebs | AWS EBS | AWS, single-AZ, high IOPS |
gcs | Google Cloud Storage | GCP |
azure | Azure Files | Azure |
cifs/smb | SMB share | Windows shares |
glusterfs | GlusterFS | On-premise clusters |
Example: AWS EFS #
# Install the plugin
docker plugin install rexray/efs:latest
# Create a volume with the EFS driver
docker volume create \
--driver rexray/efs \
--opt fsid=fs-12345678 \
app-data
# Mount into a container
docker run -v app-data:/app/data my-app
Example: NFS #
volumes:
shared-data:
driver: local
driver_opts:
type: nfs
o: addr=10.0.0.100,rw
device: ":/exports/shared"
Docker mounts the NFS share into the container, and the container can read/write like a regular volume. Good for multi-host deployments.
When to Use a Volume Driver #
| Scenario | Recommendation |
|---|---|
| Single-host development | local |
| Single-host production | local (with backups) |
| Multi-host (Swarm, K8s) | nfs, efs, gcs, azure |
| High-IOPS databases | ebs provisioned IOPS |
| Object-like access | s3, gcs (fits better) |
| Compliance/HADR | Managed databases, replicated storage |
Backing Up and Restoring Volumes #
Volumes are host directories, so backup is basically copying the directory contents. But there’s a more elegant pattern.
Backing Up with a Helper Container #
# Backup
docker run --rm \
-v mysql-data:/source:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/mysql-data-$(date +%F).tar.gz -C /source .
# Restore
docker run --rm \
-v mysql-data:/target \
-v $(pwd):/backup \
alpine \
tar xzf /backup/mysql-data-2026-06-06.tar.gz -C /target
An alpine helper container (a small image) is used for the tar operation. The source is mounted read-only for backup. The target is mounted read-write for restore.
Backing Up with Database Tools #
For databases, backing up only the volume files is often not enough. A logical backup (SQL dump) is safer:
# Backup MySQL
docker exec mysql-container \
mysqldump -u root -p mydb > mydb-$(date +%F).sql
# Restore MySQL
docker exec -i mysql-container \
mysql -u root -p mydb < mydb-2026-06-06.sql
Logical backup advantages:
- Consistent — the database is snapshotted at one point in time.
- Portable — can be restored to a different MySQL version or a different host.
- Selective — can back up just one database or one table.
Automatic Backups with Cron #
# /etc/cron.d/docker-backup
0 2 * * * root docker run --rm \
-v mysql-data:/source:ro \
-v /backup:/backup \
alpine \
tar czf /backup/mysql-$(date +\%F).tar.gz -C /source . >> /var/log/backup.log 2>&1
A host cron job runs the backup container every night. Output is logged for auditing.
Backing Up to Cloud Storage #
# Backup to S3
0 2 * * * root docker run --rm \
-v mysql-data:/source:ro \
-v /backup:/backup \
amazon/aws-cli:latest \
sh -c "tar czf /backup/db.tar.gz -C /source . && \
aws s3 cp /backup/db.tar.gz s3://my-backups/db-$(date +%F).tar.gz"
Backing up to S3 gives 11 nines of durability and access from anywhere.
Docker Volume Best Practices #
1. Use Named Volumes for Production #
# CORRECT
docker volume create app-data
docker run -v app-data:/app/data my-app
# ANTI-PATTERN (anonymous volume)
docker run -v /app/data my-app
Named volumes are easier to manage, back up, and identify.
2. Consistent Naming Conventions #
[app-name]-[data-type]-[environment]
Examples:
webapp-uploads-prodmysql-data-stagingredis-cache-prodnginx-config-prod
A consistent naming convention makes it easy to:
- Identify a volume’s owner.
- Filter by environment.
- Audit and clean up.
- Document.
3. Separate Volumes by Data Type #
volumes:
- app-data:/app/data # application data
- app-cache:/app/cache # cache
- app-logs:/app/logs # logs
- app-config:/app/config # configuration
Separation makes selective backups easy (back up only data, no need to back up caches), enables independent log rotation, and partial migration.
4. Back Up Volumes Regularly #
A volume isn’t a backup. It only moves data to the host. For important data, always back up to:
- Local but a different disk (minimum).
- An NFS share (a different location on the same network).
- S3/GCS/Azure Blob (cloud, geographically different).
- Tape/offline storage (long-term archives).
# Daily cron backup + cleanup after 30 days
0 2 * * * root /opt/scripts/backup-volumes.sh
5. Monitor Volume Usage #
# Check disk usage
docker system df
# Detail per volume
docker system df -v
# Unused volumes
docker volume ls -f dangling=true
Set up alerts in your monitoring system for volume usage above 80%.
6. Use Volume Drivers for Multi-Host #
For multi-host deployments (Swarm, K8s), the local driver isn’t enough. Volumes only exist on one host. Use:
nfsfor multi-host Linux.efsfor AWS.azurefilefor Azure.gcsfor GCP.
7. Be Careful with docker volume prune
#
# Remove unused volumes
docker volume prune
prune deletes ALL volumes not mounted to a container. Before running it:
- Make sure no important stopped containers exist.
- Check the list of volumes to be deleted (run without
--forcefirst). - Back up first if in doubt.
8. Document Mount Points #
Every volume should be documented:
Volume: pgdata-prod
Mount point in container: /var/lib/postgresql/data
Service: postgres
Contents: production PostgreSQL data
Backup: daily to S3, 30-day retention
Restore procedure: ./runbooks/postgres-restore.md
When NOT to Use Volumes #
There are situations where volumes aren’t the best choice:
- Configuration that needs editing from the host — bind mounts fit better.
- Source-code live reload — bind mounts let host source changes appear in the container immediately.
- Short config files (1-2 files) — bind mounts are simpler than named volumes.
- Caches that may be lost — tmpfs is faster.
flowchart TD
A{Need persistent<br/>storage?}
A -- No --> B[tmpfs]
A -- Yes --> C{Does the host data<br/>need direct<br/>access?}
C -- Yes --> D[Bind Mount]
C -- No --> E[Volume]
E --> F{Multi-host?}
F -- Yes --> G[Volume Driver<br/>NFS/EFS/etc.]
F -- No --> H[Local Volume]Volumes in Microservices Architectures #
In microservices architectures, each service usually has its own volumes for persistent data.
version: "3.9"
services:
api:
build: ./api
volumes:
- api-cache:/app/cache
depends_on:
- postgres
- redis
worker:
build: ./worker
volumes:
- worker-tmp:/tmp
depends_on:
- rabbitmq
postgres:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7
volumes:
- redis-data:/data
volumes:
api-cache:
worker-tmp:
pgdata:
redis-data:
The principles:
- One service, one or more clearly defined volumes.
- No volume is shared unless it genuinely needs to be.
- Databases always use volumes (or better: a managed database).
- Caches may use volumes or tmpfs depending on requirements.
- Worker temporary files may go in tmpfs.
A way to remember: Volumes are the “filing cabinets” for containers. Each container has its own cabinet for documents that must be kept. Cabinets can be moved, restructured, or shared with other containers without disturbing the documents inside.
Summary #
- Docker Volumes are the official mechanism for storing data persistently, outside the container lifecycle. Stored on the host (default
/var/lib/docker/volumes/) and managed by the Docker Engine.- Why they’re needed: without volumes, data is lost when a container is deleted, hard to back up, can’t be shared, and isn’t portable. Volumes solve all of that.
- Three storage types: volumes (for production, Docker-managed), bind mounts (for development, manual host paths), tmpfs (for sensitive caches, in RAM). Choose per use case.
- How they work: a volume = a host directory, mounted to a container path via a kernel-level bind mount. The container doesn’t know the volume’s physical location — that’s what makes volumes portable.
- CLI syntax:
docker volume create,docker volume ls,docker volume inspect,docker volume rm,docker volume prune. Mount with-v name:pathor--mount source=...,target=....- Compose syntax: define in the top-level
volumes:, mount in services withvolumes: - name:path.romode for read-only,rwfor read-write.- Named volumes > anonymous volumes. Always use named volumes with explicit names for production. Anonymous volumes (random hashes) are hard to manage.
- Volume sharing: several containers can mount the same volume. Common patterns: producer-consumer, app+backup, web server + app server. Read-only mounts for isolation.
- Volume drivers: integration with external storage (NFS, EFS, EBS, GCS, Azure). Important for multi-host production and high availability.
- Backups: a volume = a host directory, backable with
taror database tools (pg_dump,mysqldump). Back up to another location (S3, NFS) for durability.- Best practices: named volumes, consistent naming conventions, separate volumes per data type, regular backups, usage monitoring, volume drivers for multi-host, mount point documentation.
- The golden rule: Containers are stateless, data is stateful in volumes. Always mount volumes for important data, and back up to another location regularly.