Volume Lifecycle #
In container architecture, data has a different lifecycle from containers. Containers are ephemeral — easy to create and destroy. Data often needs to survive long-term, even when containers die, get recreated, or scale.
This is Docker Volumes’ main strength: it separates the data lifecycle from the container lifecycle. A volume exists before the container is created, exists after the container is deleted, and can be mounted to a new container created later. Understanding the volume lifecycle is crucial for avoiding data loss and managing storage correctly.
This article thoroughly covers the volume lifecycle: from creation, mounting, active use, dangling state, to removal. After reading this, you’ll know exactly what happens to data at every stage, and how to manage volumes safely.
The Basic Principle: Volume vs Container Lifecycle #
The most fundamental concept to hold onto:
Volumes are NOT tied to the container lifecycle.
This means:
- Containers can be created without volumes (volumes aren’t automatically created).
- Volumes are created independently of containers.
- Volumes exist before, during, and after containers live.
- Deleting a container does NOT delete the volume it mounts.
- Volumes can only be deleted explicitly.
flowchart TB
subgraph VL["Volume Lifecycle"]
V1[Created<br/>docker volume create] --> V2[Mounted<br/>container starts]
V2 --> V3[Active<br/>read/write]
V3 --> V4[Unmounted<br/>container stops/removed]
V4 --> V5a[Mounted Again<br/>new container]
V4 --> V5b[Dangling<br/>no container]
V5a --> V3
V5b --> V6[Removed<br/>docker volume rm]
end
subgraph CL["Container Lifecycle"]
C1[Created] --> C2[Running]
C2 --> C3[Stopped/Removed]
C3 --> C4[*]
end
VL -.->|independent| CL
style V1 fill:#a8d8a8
style V2 fill:#a8d8a8
style V3 fill:#a8d8a8
style V6 fill:#ff6b6b,color:#fffThis cycle explains many important things:
- A dead container doesn’t mean lost data.
- A new container can mount a volume that already has content.
- Unused volumes become “dangling” — still there, but unattached.
- Volumes only disappear if you explicitly delete them.
The golden principle: Containers may die, but data must stay alive. Volumes are the mechanism that guarantees this principle.
Volume Lifecycle Stages #
1. Volume Creation #
Volumes can be created in two ways: explicitly or implicitly.
Explicit (Manual) #
docker volume create mydata
Characteristics:
- The volume stands alone, not tied to any specific container.
- Docker creates a directory at
/var/lib/docker/volumes/mydata/_data/. - Safe for long-term data — the volume already exists before the first container.
- More explicit and documented.
Output:
mydata
Implicit (Automatic by Docker) #
docker run -v mydata:/app/data nginx
If mydata doesn’t exist, Docker automatically creates the volume. Characteristics:
- Practical for development or quick experiments.
- Not documented — needs checking with
docker volume ls. - Prone to dangling volumes when containers are frequently created and deleted.
flowchart TB
A[docker run -v mydata:/app/data nginx] --> B{Does the mydata<br/>volume exist?}
B -- Yes --> C[Mount the existing volume]
B -- No --> D[Create volume automatically]
D --> C
C --> E[Container runs]Naming Conventions for Clarity #
For production, always use explicit names with a clear convention:
docker volume create webapp-uploads-prod
docker volume create mysql-data-prod
docker volume create redis-cache-prod
This makes auditing and backups easier. You can immediately tell which volume holds what.
2. Volume Mounting #
When a container runs with a volume mount, Docker mounts the volume directory to a path in the container.
docker run -v mysql-data:/var/lib/mysql mysql:8
What happens internally:
- The Docker daemon receives the mount instruction.
- The daemon looks for the
mysql-datavolume in/var/lib/docker/volumes/. - The daemon creates a mount point in the container (defaulting to
/var/lib/mysql). - The container sees this path as an ordinary directory.
The container doesn’t know its path is a volume. All it sees is a readable, writable directory.
flowchart LR
HOST[Host:<br/>/var/lib/docker/volumes/<br/>mysql-data/_data/] -->|bind mount<br/>kernel level| CONT[Container:<br/>/var/lib/mysql/]Technical detail: Docker uses a kernel-level bind mount to attach the volume directory to the container path. This isn’t a copy — when the container reads /var/lib/mysql/foo.txt, it directly reads the host file. No synchronization overhead.3. Active Use — the Volume in Use #
While the container runs, the volume is in active status. At this stage:
- The container can read/write data at the mount point.
- If the volume is mounted to many containers, all containers share the same data.
- Changes in one container are immediately visible to other containers mounting the same volume.
services:
app:
image: my-app
volumes:
- shared-data:/app/data
worker:
image: my-worker
volumes:
- shared-data:/input
When app writes a file at /app/data/file.txt, worker can immediately read the same file at /input/file.txt.
flowchart LR
A[App Container<br/>write] --> VOL[shared-data]
VOL --> B[Worker Container<br/>read]
A -.->|immediate changes| BAccess Modes: Read-Write vs Read-Only #
Containers can mount a volume in two modes:
- Read-Write (default) — the container can read and write.
- Read-Only — the container can only read, not write.
# Read-write
docker run -v shared-data:/app/data my-app
# Read-only
docker run -v shared-data:/input:ro my-worker
The common pattern: producers use read-write, consumers use read-only. This provides isolation — a consumer can’t corrupt data even if compromised.
4. Container Stop or Remove #
docker stop mycontainer
docker rm mycontainer
What happens:
- The container disappears.
- The volume remains with all its contents.
- The kernel releases the volume mount.
You can inspect the volume to see its contents:
docker volume inspect mysql-data
And access it directly from the host:
ls /var/lib/docker/volumes/mysql-data/_data/
flowchart TB
C1[Container A<br/>running] --> V[Volume<br/>active]
C2[Container B<br/>running] --> V
C1 -.->|stop/remove| GONE[Container A gone]
C2 --> V
V --> V2[Volume stays active<br/>because B still mounts it]
style GONE fill:#ff6b6b,color:#fff
style V2 fill:#51cf66,color:#fffThis is the heart of the volume lifecycle — data is not lost when the container is lost.
Exceptions:docker compose down -vremoves volumes along with containers. Anddocker volume rm nameremoves a volume explicitly. Outside of those, the container lifecycle doesn’t remove volumes.
5. Dangling Volumes — Unused Volumes #
If all containers mounting a volume have been removed, the volume becomes dangling:
- The volume still exists on the host.
- No container mounts it.
- Unused, but not deleted.
- Consumes disk space without benefit.
# List dangling volumes
docker volume ls -f dangling=true
Output:
local my-old-app-data
local leftover-volume
local test-volume-abc
These volumes often pile up on development servers or CI/CD environments that frequently create and delete containers.
flowchart TB
V[Volume] --> C1[Container A]
V --> C2[Container B]
C1 -.->|removed| X[Container A removed]
C2 -.->|removed| Y[Container B removed]
X --> D[Volume still exists,<br/>now dangling]
Y --> D
D --> P[Not deleted yet,<br/>eats disk]
style D fill:#ffd43b
style P fill:#ffd43bThe Impact of Dangling Volumes #
- Wasted disk space — unused volumes still consume storage.
- Hard to audit — without cleanup, the volume list gets confusing.
- Orphan data risk — important volumes that were forgotten could be mistakenly pruned.
How to Handle Dangling Volumes #
# First see what will be deleted
docker volume ls -f dangling=true
# Delete all dangling volumes
docker volume prune
# Delete with automatic confirmation
docker volume prune --force
# Delete unused volumes with a specific label
docker volume prune --filter "label=env=staging"
Caution: docker volume prune deletes ALL volumes not mounted to a container (running or stopped). If you have important volumes currently unused (e.g. a service scaled down), they’ll be deleted. Always back up before pruning.6. Volume Removal #
A volume can only be removed if it’s not currently mounted to any container.
# Remove one volume
docker volume rm mysql-data
# Output on success:
# mysql-data
# Output on failure (still mounted):
# Error response from daemon: remove mysql-data: volume is in use
# Remove multiple volumes
docker volume rm vol1 vol2 vol3
# Remove all unused volumes
docker volume prune
What happens when a volume is removed:
- Docker checks whether the volume is mounted to any container.
- If yes, it returns an error and doesn’t remove it.
- If no, Docker removes the volume directory on the host.
- The
_datadirectory and its contents are permanently gone.
flowchart TB
A[docker volume rm mydata] --> B{Volume currently<br/>mounted?}
B -- Yes --> C[Error:<br/>volume is in use]
B -- No --> D[Confirmation]
D --> E[--force?] -- Yes --> F[Remove]
E -- No --> D
F --> G[Data permanently lost]
style G fill:#ff6b6b,color:#fffTo remove a mounted volume, you must:
# 1. Stop the container mounting it
docker stop container-name
# 2. Remove the container
docker rm container-name
# 3. Now the volume can be removed
docker volume rm mydata
Or force it by removing the container without a graceful shutdown:
# Force remove container (volume removal also possible after)
docker rm -f container-name
docker volume rm mydata
There’s no undo. Once a volume is removed, its data is gone. No recycle bin, no built-in recovery tool. If you need the data, back it up first.
The Lifecycle in Docker Compose #
In Docker Compose, the volume lifecycle differs slightly from pure CLI.
Volumes Declared in the Compose File #
services:
db:
image: postgres:16
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
The Compose Lifecycle #
# Create containers + volumes
docker compose up -d
# - Containers created
# - Volume 'dbdata' created if it doesn't exist
# - Volume mounted to containers
# Stop and remove containers (volumes REMAIN)
docker compose down
# - Containers removed
# - Networks removed
# - Volumes REMAIN
# Stop + remove containers + remove volumes
docker compose down -v
# - Containers removed
# - Networks removed
# - Volumes also REMOVED
flowchart LR
UP[compose up -d] --> RUNNING[Container running<br/>volume mounted]
DOWN[compose down] --> STOPPED[Container removed<br/>volume REMAINS]
DOWNV[compose down -v] --> GONE[Container removed<br/>volume DELETED]
style GONE fill:#ff6b6b,color:#fff
style STOPPED fill:#51cf66,color:#fffBe careful withcompose down -v: this command removes the volumes defined in the compose file. For production data, you almost always want NOT to use the-vflag. Volumes must remain afterdownso data persists.
Inspecting Compose Volumes #
# List all defined volumes
docker compose config --volumes
# Inspect a specific volume
docker volume inspect <projectname>_dbdata
In Compose, volume names are prefixed with the project name (default: the directory name). So dbdata in the compose file becomes myproject_dbdata in Docker.
Backing Up Compose Volumes #
services:
db:
image: postgres:16
volumes:
- dbdata:/var/lib/postgresql/data
backup:
image: alpine
profiles: ["backup"] # only runs with --profile backup
volumes:
- dbdata:/source:ro
- ./backups:/backup
command: tar czf /backup/dbdata-$(date +%F).tar.gz -C /source .
# Manual backup
docker compose --profile backup run --rm backup
Pruning Storage — Routine Cleanup #
Docker provides several commands for cleanup:
| Command | Function | Danger |
|---|---|---|
docker container prune | Remove stopped containers | Low |
docker image prune | Remove unused images | Low |
docker volume prune | Remove dangling volumes | High (data loss) |
docker network prune | Remove unused networks | Low |
docker system prune | Remove all of the above | High |
docker system prune --volumes | Remove including volumes | Very high |
# Dry run — see what would be deleted
docker system prune --volumes --dry-run
# Prune with a filter
docker system prune --volumes --filter "until=24h" # older than 24 hours
Best practice before pruning:
- Back up important volumes.
- Inspect the list to be deleted with
--dry-runfirst.- Make sure no important services are stopped.
- Test a restore from backup before pruning in production.
Monitoring the Volume Lifecycle #
To manage volumes properly in production, you need visibility. Here are tools you can use:
Docker System DF #
# Overall disk usage
docker system df
# Detail per item
docker system df -v
Output:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 15 5 2.5GB 1.2GB (48%)
Containers 10 2 500MB 300MB (60%)
Local Volumes 8 3 4.2GB 1.5GB (35%)
Build Cache 5 0 800MB 800MB
A Monitoring Script #
#!/bin/bash
# monitoring-volumes.sh
# Run via cron, send an alert if volume usage is high
# Collect volume data
docker volume ls -q | while read vol; do
size=$(docker run --rm -v $vol:/check:ro alpine du -sh /check 2>/dev/null | awk '{print $1}')
echo "Volume: $vol, Size: $size"
done
# Check dangling volumes
dangling=$(docker volume ls -f dangling=true -q | wc -l)
echo "Dangling volumes: $dangling"
# Alert if any volume is over 80% usage
# (depends on your monitoring setup)
Integration with a Monitoring Stack #
For production, send volume data to Prometheus/Grafana or another monitoring stack:
# docker-compose.yml for monitoring
services:
node-exporter:
image: prom/node-exporter
volumes:
- /var/lib/docker/volumes:/var/lib/docker/volumes:ro
command:
- '--path.rootfs=/host'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
Volume Lifecycle Best Practices #
1. Back Up Before Cleanup #
# Back up the volume before pruning
docker run --rm \
-v mydata:/source:ro \
-v /backup:/backup \
alpine tar czf /backup/mydata-$(date +%F).tar.gz -C /source .
# Then prune
docker volume prune
2. Clear Naming Conventions #
[app]-[data-type]-[environment]
Examples: webapp-uploads-prod, mysql-data-staging.
3. Document Mount Points #
Every volume should have a record:
Volume: pgdata-prod
Mount point: /var/lib/postgresql/data
Service: postgres
Backup: daily to S3, 30-day retention
Retention: 90 days (auto-prune after that)
Restore: ./runbooks/postgres-restore.md
4. Audit Volumes Regularly #
# Weekly audit
docker volume ls
docker volume ls -f dangling=true
docker system df -v
Clean up dangling volumes routinely, but back up the important ones first.
5. Don’t Blindly Run docker volume prune
#
# DON'T:
docker volume prune --force # deletes immediately without confirmation
# BETTER:
docker volume ls -f dangling=true # look first
docker volume prune # with confirmation
6. Test Restores Regularly #
A volume backup that’s never restored = a backup that doesn’t exist. Restore to a test environment regularly for verification.
7. Use Compose Profiles for Temporary Services #
services:
db:
image: postgres:16
volumes:
- dbdata:/var/lib/postgresql/data
backup:
profiles: ["backup"] # only runs with --profile backup
image: alpine
volumes:
- dbdata:/source:ro
- ./backups:/backup
command: tar czf /backup/db.tar.gz -C /source .
# Main services
docker compose up -d
# Manual backup
docker compose --profile backup run --rm backup
The backup service doesn’t automatically run on compose up; only when the profile is activated. This prevents misconfigured backup services.
8. Monitor Volume Usage #
Set up alerts in your monitoring system:
- Volume usage > 80% → warning.
- Volume usage > 95% → critical.
- Dangling volume count > 10 → cleanup needed.
- Abnormal volume creation rate → check whether a service is misbehaving.
9. Lifecycle Automation #
For large environments, automate lifecycle management:
# Cron: regular backup + prune
0 2 * * * /opt/scripts/backup-volumes.sh
0 4 * * 0 /opt/scripts/cleanup-dangling.sh # weekly, after backup
#!/bin/bash
# cleanup-dangling.sh
# Remove dangling volumes that have been backed up
# Back up first
/opt/scripts/backup-volumes.sh
# Wait for the backup to finish
sleep 60
# Check for volumes without containers
for vol in $(docker volume ls -q); do
if ! docker ps -a --filter volume=$vol -q | grep -q .; then
echo "Dangling volume: $vol"
# Only remove if it has a specific label (e.g. cleanup-eligible)
if docker volume inspect $vol --format '{{index .Labels "cleanup-eligible"}}' | grep -q "true"; then
docker volume rm $vol
fi
fi
done
10. Consider Volume Drivers for Auto-Cleanup #
Some volume drivers (especially cloud-native ones) support TTL and auto-cleanup:
volumes:
cache-data:
driver: amazon/ebs
driver_opts:
size: 100
type: gp3
# Auto-delete the volume when the container stops
# Useful for caches that may be lost
Common Lifecycle Patterns #
Pattern 1: Production Database #
1. docker compose up -d
→ db container created
→ pgdata volume created (first time) or mounted
→ Database starts, writes data to the volume
2. Daily backup (cron)
→ pg_dump to S3
3. Image update (new deploy)
→ docker compose pull
→ docker compose up -d
→ Containers recreated
→ pgdata volume REMAINS, database persists
4. Migration to a new host
→ Back up pgdata to a file
→ Copy the file to the new host
→ docker volume create pgdata on the new host
→ Restore the backup to the volume
→ docker compose up -d
→ Database runs with the same data
Pattern 2: Development with an Ephemeral + Persistent Mix #
services:
app:
volumes:
- ./src:/app/src # bind mount, source code, ephemeral
- node_modules:/app/node_modules # volume, dependencies, persistent
- ./.env:/app/.env:ro # bind mount, config, read-only
- Source code — bind mount, immediately visible on the host, no rebuild needed.
- node_modules — volume, must not be overwritten by the host, persistent.
- Config — read-only bind mount, source of truth on the host.
Pattern 3: Auto-Cleanup Caches #
volumes:
cache-data:
labels:
cleanup-eligible: "true"
A weekly cleanup script removes dangling volumes with the cleanup-eligible=true label. Mistakenly pruned caches aren’t a problem since they can be rebuilt.
Pattern 4: Multi-Environment with Different Lifecycles #
# docker-compose.prod.yml
services:
db:
volumes:
- pgdata-prod:/var/lib/postgresql/data
volumes:
pgdata-prod:
name: myapp-pgdata-prod # explicit name, not project-prefixed
# docker-compose.staging.yml
services:
db:
volumes:
- pgdata-staging:/var/lib/postgresql/data
volumes:
pgdata-staging:
name: myapp-pgdata-staging
Production and staging have volumes with different names, manageable independently. docker compose -f prod.yml down -v won’t delete the staging volumes.
Common Mistakes and How to Avoid Them #
1. Assuming docker rm Removes Volumes
#
# ANTI-PATTERN
docker rm -f container-name
# The developer thinks data is gone, but the volume still exists
# Check:
docker volume ls
# The volume is still there!
Fix: Always check docker volume ls after docker rm to make sure cleanup happened.
2. Forgetting to Back Up Before prune
#
# ANTI-PATTERN
docker volume prune --force
# An important volume that was forgotten to be mounted is instantly gone
Fix: Always back up first, or filter pruning to specific labels.
3. compose down -v in Production
#
# ANTI-PATTERN
docker compose -f prod.yml down -v
# All production volumes are gone!
Fix: Drop -v for production. Use plain down so volumes remain.
4. Not Inspecting a Volume Before Removal #
# ANTI-PATTERN
docker volume rm some-volume
# The developer thinks "some-volume" isn't important
# Turns out it contains production data
Fix: Always inspect first:
docker volume inspect some-volume
ls /var/lib/docker/volumes/some-volume/_data/ # check its contents
5. Not Knowing About Dangling Volumes #
A production server fills up after a few months because dangling volumes pile up. No routine cleanup.
Fix: Set up monitoring and cleanup automation.
Summary #
- The volume lifecycle is independent of the container. Containers can be created and deleted; volumes remain. This is Docker Volumes’ main strength — data isn’t lost when the container is lost.
- Three ways to create volumes: explicit (
docker volume create), implicit (duringdocker run -v name:/path), or via Compose. For production, always use explicit creation with clear names.- Four volume states: Created (newly made, not yet mounted), Mounted (a container mounts the volume), Active (read/write activity), Unmounted (no container mounts it).
- A dangling volume is one not mounted to any container. It still eats disk but serves no purpose. Clean up with
docker volume prune, but BACK UP FIRST.- Volume removal can only happen when the volume isn’t mounted.
docker volume rmfails with “volume is in use” if still in use. Stop and remove the container first, or force with-f.docker compose downdoes NOT remove volumes.docker compose down -vis what removes them. For production, you almost always wantdownwithout-v.- Consistent naming conventions make audit, backup, and cleanup easier. Pattern:
[app]-[data-type]-[environment], e.g.webapp-uploads-prod.- Monitoring:
docker system dfanddocker system df -vshow storage usage. Set up alerts for high volume usage and accumulating dangling volumes.- Best practices: back up before pruning, audit volumes routinely, document mount points, test restores regularly, use Compose profiles for backup services, monitor usage, and automate cleanup carefully.
- The golden rule: Volumes may exist without containers, containers may exist without volumes, but volumes can ONLY disappear through explicit deletion. That’s what makes data persist in the container world.