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:#ffd8a8

Volumes 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:#fff

Five problems volumes solve:

  1. Persistence — data survives outside the container lifecycle.
  2. Backup — data can be backed up easily (volume = a host directory).
  3. Sharing — many containers can mount the same volume.
  4. Migration — data can be moved to another host (volume = a host folder).
  5. 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.

MechanismLocationPersistentManaged ByUse Case
Volume/var/lib/docker/volumes/YesDockerProduction, databases, multi-container
Bind MountManual host pathYesUser / OSDevelopment, live reload, config
tmpfsRAMNoOSCaches, 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:#a8d8a8

Each 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 rm and docker volume prune permanently 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:#ffd8a8
Important 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 #

DriverStorageUse Case
localLocal filesystem (default)Single-host deployment
nfsNFS shareMulti-host Linux
amazon/efsAWS EFSAWS, multi-AZ
amazon/ebsAWS EBSAWS, single-AZ, high IOPS
gcsGoogle Cloud StorageGCP
azureAzure FilesAzure
cifs/smbSMB shareWindows shares
glusterfsGlusterFSOn-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 #

ScenarioRecommendation
Single-host developmentlocal
Single-host productionlocal (with backups)
Multi-host (Swarm, K8s)nfs, efs, gcs, azure
High-IOPS databasesebs provisioned IOPS
Object-like accesss3, gcs (fits better)
Compliance/HADRManaged 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-prod
  • mysql-data-staging
  • redis-cache-prod
  • nginx-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:

  • nfs for multi-host Linux.
  • efs for AWS.
  • azurefile for Azure.
  • gcs for 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 --force first).
  • 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:path or --mount source=...,target=....
  • Compose syntax: define in the top-level volumes:, mount in services with volumes: - name:path. ro mode for read-only, rw for 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 tar or 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.

← Previous: Data Loss   Next: Volume Lifecycle →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact