Docker Volumes 2025 — Persistent Data Management for Containers

Sanjeev SharmaSanjeev Sharma
7 min read

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:

TypeStoredManaged ByBest For
Named VolumeDocker managed pathDockerDatabase data, production persistence
Bind MountAny host pathUserLocal dev, config files, hot reload
tmpfs MountHost RAM onlyKernelTemporary 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-alpine

Volume 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 -f

Bind 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 container

Bind 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-alpine

tmpfs 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.0

Volumes 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.0

In 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:
      - app

Backup 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 mydb

Volume 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.0

In 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 -v unintentionally 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 -v regularly 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 prune in 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 prune removes 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_dump for logical PostgreSQL backups and volume tar archives for filesystem-level backups
  • In Kubernetes, named volumes map conceptually to PersistentVolumeClaims backed by StorageClasses

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading