Docker Volume Management: Backups, Migration, and Best Practices

You’ve got a dozen containers running — Nextcloud, Jellyfin, Paperless-ngx, databases. Each one stores data in Docker volumes. But can you actually back them up? Migrate them to a new server? Clean up the dead weight?

If you already understand the difference between volumes and bind mounts, this guide picks up where that leaves off. We’re covering the operational side: how to protect, move, and maintain your Docker volumes in production.


Quick Volume Inventory

Before managing anything, know what you have:

# List all volumes
docker volume ls

# Show volume details (mount point, driver, labels)
docker volume inspect my_volume

# Find volumes not attached to any container
docker volume ls -f dangling=true

# See disk usage per volume
docker system df -v | grep "VOLUME NAME" -A 1000

For a full picture, combine with du:

# Check actual size of all volumes
sudo du -sh /var/lib/docker/volumes/*/

Backing Up Docker Volumes

This is the most important section. If you’re not backing up your volumes, you’re one docker volume rm or disk failure away from losing everything.

Method 1: Temporary Container with tar (Universal)

The most reliable approach — works with any volume type:

# Stop the container using the volume first (recommended)
docker stop myapp

# Back up the volume to a tar archive
docker run --rm \
  -v myapp_data:/source:ro \
  -v $(pwd)/backups:/backup \
  alpine tar czf /backup/myapp_data_$(date +%Y%m%d_%H%M%S).tar.gz -C /source .

# Restart the container
docker start myapp

What’s happening: We spin up a temporary Alpine container, mount the volume as read-only, and tar everything to a backup directory on the host.

Method 2: Direct Path Copy (Faster, Linux Only)

Docker stores volumes at /var/lib/docker/volumes/ on Linux. You can copy directly:

# Find the volume's mount point
docker volume inspect myapp_data --format '{{ .Mountpoint }}'
# → /var/lib/docker/volumes/myapp_data/_data

# Copy with rsync (preserves permissions)
sudo rsync -a /var/lib/docker/volumes/myapp_data/_data/ /backups/myapp_data/

Caveat: This bypasses Docker’s storage driver abstraction. It works for the default local driver but may not for custom drivers (like NFS or cloud volumes).

Method 3: Database-Specific Dumps

For databases, a file-level copy of the volume might produce a corrupted backup if the database is writing. Always use the database’s own dump tool:

# PostgreSQL
docker exec mydb pg_dump -U postgres mydb > backup.sql

# MariaDB / MySQL
docker exec mydb mariadb-dump -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases > backup.sql

# MongoDB
docker exec mydb mongodump --archive=/tmp/backup.gz --gzip
docker cp mydb:/tmp/backup.gz ./backup.gz

# Redis
docker exec myredis redis-cli BGSAVE
docker cp myredis:/data/dump.rdb ./dump.rdb

Automated Backup Script

Here’s a script that backs up all volumes for a given compose project:

#!/bin/bash
# backup-volumes.sh — Back up all volumes for a Docker Compose project
set -euo pipefail

PROJECT="${1:?Usage: backup-volumes.sh <project-name>}"
BACKUP_DIR="/backups/docker-volumes/${PROJECT}/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"

# Get all volumes for this project
VOLUMES=$(docker volume ls --filter "label=com.docker.compose.project=${PROJECT}" -q)

if [ -z "$VOLUMES" ]; then
  echo "No volumes found for project: $PROJECT"
  exit 1
fi

echo "Backing up volumes for project: $PROJECT"
echo "Destination: $BACKUP_DIR"

for VOL in $VOLUMES; do
  echo "  → $VOL"
  docker run --rm \
    -v "${VOL}:/source:ro" \
    -v "${BACKUP_DIR}:/backup" \
    alpine tar czf "/backup/${VOL}.tar.gz" -C /source .
done

echo "✅ Backed up $(echo "$VOLUMES" | wc -w) volumes to $BACKUP_DIR"

# Optional: remove backups older than 30 days
find "/backups/docker-volumes/${PROJECT}" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

Make it executable and add to cron:

chmod +x backup-volumes.sh

# Run daily at 3 AM
echo "0 3 * * * /opt/scripts/backup-volumes.sh myproject >> /var/log/volume-backup.log 2>&1" | crontab -

Restoring Volumes from Backup

Restore from tar Archive

# Create the volume if it doesn't exist
docker volume create myapp_data

# Restore from backup
docker run --rm \
  -v myapp_data:/target \
  -v $(pwd)/backups:/backup:ro \
  alpine sh -c "cd /target && tar xzf /backup/myapp_data_20260321.tar.gz"

Restore Database Dumps

# PostgreSQL
cat backup.sql | docker exec -i mydb psql -U postgres

# MariaDB / MySQL
cat backup.sql | docker exec -i mydb mariadb -u root -p"$MYSQL_ROOT_PASSWORD"

# MongoDB
docker cp backup.gz mydb:/tmp/backup.gz
docker exec mydb mongorestore --archive=/tmp/backup.gz --gzip --drop

Verify Your Restores

A backup you haven’t tested is not a backup. Periodically verify:

# Spin up a temporary container with the restored volume
docker run --rm -v myapp_data:/data alpine ls -la /data/

# For databases, restore to a temp container and run a query
docker run --rm -d --name test-pg \
  -v pgdata_restored:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=test \
  postgres:16
sleep 5
docker exec test-pg psql -U postgres -c "SELECT count(*) FROM important_table;"
docker rm -f test-pg

Migrating Volumes Between Hosts

Moving your self-hosted stack to a new server? Here’s how to bring your data along.

Method 1: SSH + tar (Simple)

Pipe a volume backup directly to the new host:

# On the source host — stream volume to remote
docker run --rm \
  -v myapp_data:/source:ro \
  alpine tar czf - -C /source . | \
  ssh user@newhost "docker run --rm -i -v myapp_data:/target alpine sh -c 'cd /target && tar xzf -'"

One command. The volume is created and populated on the remote host.

Method 2: rsync (Resumable, Best for Large Volumes)

For multi-gigabyte volumes (media libraries, photo archives):

# Get the local volume path
SRC=$(docker volume inspect myapp_data --format '{{ .Mountpoint }}')

# rsync to remote Docker volume path
sudo rsync -avz --progress \
  "$SRC/" \
  user@newhost:/var/lib/docker/volumes/myapp_data/_data/

Pro tip: rsync can resume interrupted transfers. For a 500GB Jellyfin library, this is much better than tar piping.

Method 3: Backup File + Transfer (Most Reliable)

The safest approach for critical data:

# 1. Stop services
docker compose down

# 2. Back up all volumes
./backup-volumes.sh myproject

# 3. Transfer backup files
rsync -avz /backups/docker-volumes/myproject/ user@newhost:/backups/

# 4. On new host: create volumes and restore
for f in /backups/*.tar.gz; do
  VOL=$(basename "$f" .tar.gz)
  docker volume create "$VOL"
  docker run --rm -v "${VOL}:/target" -v /backups:/backup:ro \
    alpine sh -c "cd /target && tar xzf /backup/$(basename $f)"
done

# 5. Copy your docker-compose.yml and .env, then start
docker compose up -d

Migration Checklist

Before decommissioning the old host:

  • All volumes backed up and transferred
  • Database dumps taken (don’t rely only on volume copies)
  • Compose files and .env files copied
  • DNS/reverse proxy updated to point to new IP
  • SSL certificates transferred or regenerated
  • Services tested on new host
  • Old host kept running for 1 week as fallback

Cleanup and Maintenance

Docker volumes accumulate over time. Old projects, failed experiments, test containers — they all leave volumes behind.

Finding Orphaned Volumes

# Volumes not attached to any container
docker volume ls -f dangling=true

# More aggressive: find volumes with no matching compose project
docker volume ls --format '{{.Name}}' | while read vol; do
  CONTAINERS=$(docker ps -a --filter "volume=$vol" -q)
  if [ -z "$CONTAINERS" ]; then
    SIZE=$(sudo du -sh "/var/lib/docker/volumes/$vol/_data" 2>/dev/null | cut -f1)
    echo "ORPHAN: $vol ($SIZE)"
  fi
done

Safe Cleanup

# Remove dangling volumes (not attached to any container)
docker volume prune

# Nuclear option: remove ALL unused volumes (even named ones)
# ⚠️ Only if you're sure no stopped container needs them
docker volume prune -a

# Remove specific volumes
docker volume rm old_project_db_data old_project_redis_data

Warning: docker volume prune -a removes volumes for stopped containers too. If you regularly stop services (e.g., for maintenance), this will destroy their data. Use with extreme caution.

Monitoring Volume Size

Add this to your monitoring routine:

#!/bin/bash
# volume-report.sh — Weekly volume size report
echo "=== Docker Volume Report — $(date) ==="
echo ""
printf "%-50s %10s\n" "VOLUME" "SIZE"
printf "%-50s %10s\n" "------" "----"

docker volume ls -q | while read vol; do
  SIZE=$(sudo du -sh "/var/lib/docker/volumes/$vol/_data" 2>/dev/null | cut -f1)
  printf "%-50s %10s\n" "$vol" "${SIZE:-N/A}"
done | sort -t$'\t' -k2 -h -r

echo ""
echo "Total: $(sudo du -sh /var/lib/docker/volumes/ | cut -f1)"

Production Best Practices

1. Name Your Volumes Explicitly

Don’t let Compose auto-generate names like myproject_db-data. Set them explicitly:

volumes:
  db-data:
    name: myapp-db-data  # Predictable, survives project renames

2. Use Labels for Organization

volumes:
  db-data:
    name: myapp-db-data
    labels:
      project: myapp
      backup: "true"
      backup-method: pg_dump
      criticality: high

Then query by label:

# Find all volumes that need backup
docker volume ls --filter "label=backup=true"

# Find critical volumes
docker volume ls --filter "label=criticality=high"

3. Separate Data from Configuration

Mount configuration as bind mounts (easy to version control) and data as volumes (managed by Docker):

services:
  myapp:
    volumes:
      - ./config:/app/config:ro      # Config: bind mount, in git
      - app-data:/app/data            # Data: Docker volume, backed up
      - app-cache:/app/cache          # Cache: Docker volume, disposable

4. Read-Only Where Possible

If a container only reads from a volume, mount it read-only:

services:
  web:
    volumes:
      - static-assets:/usr/share/nginx/html:ro

5. Set Up the 3-2-1 Backup Rule

For critical self-hosted data:

  • 3 copies of your data
  • 2 different storage types (local + cloud)
  • 1 offsite copy

Combine volume backups with a tool like Kopia or Duplicati for encrypted offsite backups.

6. Document Your Volume Map

Maintain a simple reference of what’s where:

| Service      | Volume              | Type    | Backup Method | Critical |
|-------------|---------------------|---------|---------------|----------|
| Nextcloud   | nextcloud-data      | Volume  | tar + rsync   | Yes      |
| PostgreSQL  | postgres-data       | Volume  | pg_dump       | Yes      |
| Jellyfin    | jellyfin-config     | Volume  | tar           | Medium   |
| Media       | /mnt/media          | Bind    | rsync to NAS  | Yes      |
| Redis       | redis-data          | Volume  | None (cache)  | No       |

Troubleshooting

“volume is in use” Error When Removing

# Find which containers use the volume
docker ps -a --filter "volume=myvolume"

# Stop/remove those containers first
docker rm -f <container_id>
docker volume rm myvolume

Permission Issues After Restore

Containers often run as non-root users. After restoring, fix ownership:

# Check what user the container expects
docker exec myapp id
# → uid=1000(app) gid=1000(app)

# Fix permissions in the volume
docker run --rm -v myapp_data:/data alpine chown -R 1000:1000 /data

Volume Data Corrupted After Crash

If a host crash left volume data in a bad state:

# For databases: most have recovery modes
docker run --rm -v postgres_data:/var/lib/postgresql/data \
  postgres:16 postgres --single -D /var/lib/postgresql/data

# For file-based volumes: check filesystem
sudo fsck /dev/sdX  # if volume is on a dedicated partition

Volume Taking Too Much Space

# Find what's eating space inside the volume
docker run --rm -v myapp_data:/data alpine du -sh /data/* | sort -rh | head -20

# Common culprits: logs, cache, temp files
docker run --rm -v myapp_data:/data alpine find /data -name "*.log" -size +100M

Wrapping Up

Docker volumes aren’t complicated, but managing them well takes discipline. The key habits:

  1. Back up regularly — automated, tested, offsite
  2. Label everything — your future self will thank you
  3. Clean up periodically — orphaned volumes waste disk
  4. Test restores — an untested backup is a wish
  5. Document your volume map — know where your data lives

If you’re building a new self-hosted stack, also check out our Docker Healthchecks guide for keeping your containers running smoothly and Kopia for a complete backup solution that can handle your volume archives.

Your data is only as safe as your worst backup strategy. Make it a good one.