Docker Backup Strategies: How to Protect Your Containers and Volumes
Containers are ephemeral by design, but the data they generate and store is not. A misconfigured docker compose down -v, a failed disk, or a botched update can wipe out databases, user uploads, and configuration files in seconds. Yet many teams treat Docker backups as an afterthought, assuming that "infrastructure as code" means everything can be recreated from scratch.
It cannot. Your Compose files can recreate containers, but they cannot recreate the data inside your PostgreSQL volume, the uploaded files in your media directory, or the state of your Redis cache. This guide covers practical, battle-tested strategies for backing up every critical component of your Docker infrastructure.
What Exactly Needs Backing Up?
Before setting up backup automation, you need to understand what data in a Docker environment is worth preserving:
| Component | Backup Priority | Method |
|---|---|---|
| Named volumes (databases, uploads) | Critical | Volume backup or database dump |
| Bind mounts (config, data) | Critical | Filesystem backup |
| Compose files and .env | High | Git repository |
| Custom Docker images | Medium | Registry or Dockerfile in Git |
| Container configurations | Medium | docker inspect exports |
| Docker networks and secrets | Low | Recreatable from config |
| Container filesystems (writable layer) | Low | Usually not needed |
Rule of thumb: If deleting a container and recreating it from the same image would lose data you care about, that data needs a backup strategy.
Volume Backup Methods
Docker volumes are the primary persistence mechanism for containers. There are several approaches to backing them up, each with different trade-offs.
Method 1: The Temporary Container Approach
The most common method uses a temporary container to mount the volume and create a tar archive:
# Backup a named volume to a tar.gz file
docker run --rm \
-v my_postgres_data:/source:ro \
-v $(pwd)/backups:/backup \
alpine tar czf /backup/postgres_data_$(date +%Y%m%d_%H%M%S).tar.gz \
-C /source .
# Restore from backup
docker run --rm \
-v my_postgres_data:/target \
-v $(pwd)/backups:/backup \
alpine sh -c "cd /target && tar xzf /backup/postgres_data_20250208_120000.tar.gz"
The :ro flag mounts the volume as read-only during backup, which is safer but may not be sufficient for databases that are actively writing data. For consistent database backups, use the application-native dump methods described below.
Method 2: Docker Volume Copy with cp
For quick one-off backups, you can copy data directly from a running container:
# Copy a directory from a running container
docker cp my_container:/var/lib/data ./backup_data/
# Copy back for restore
docker cp ./backup_data/. my_container:/var/lib/data/
This approach is simpler but offers less control over compression and does not guarantee data consistency for databases.
Method 3: Host Path Snapshots
If you use bind mounts or your Docker storage driver supports it, you can use filesystem-level snapshots:
# For LVM-backed storage
lvcreate --snapshot --name docker_snap \
--size 10G /dev/vg0/docker_data
# Mount and backup the snapshot
mount /dev/vg0/docker_snap /mnt/snapshot
tar czf /backups/docker_volumes_$(date +%Y%m%d).tar.gz \
-C /mnt/snapshot .
# Clean up
umount /mnt/snapshot
lvremove -f /dev/vg0/docker_snap
For ZFS or Btrfs filesystems, snapshots are even more efficient since they are copy-on-write and nearly instantaneous.
Database Container Backups
Databases require special attention because a raw volume copy of a running database can produce a corrupted backup. Always use the database's native dump tools.
PostgreSQL
# Dump a PostgreSQL database from a running container
docker exec my_postgres pg_dump -U postgres mydb | \
gzip > backups/mydb_$(date +%Y%m%d_%H%M%S).sql.gz
# Dump all databases
docker exec my_postgres pg_dumpall -U postgres | \
gzip > backups/all_databases_$(date +%Y%m%d_%H%M%S).sql.gz
# Restore
gunzip -c backups/mydb_20250208_120000.sql.gz | \
docker exec -i my_postgres psql -U postgres mydb
MySQL / MariaDB
# Dump a MySQL database
docker exec my_mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction --routines --triggers mydb | \
gzip > backups/mydb_$(date +%Y%m%d_%H%M%S).sql.gz
# Dump all databases
docker exec my_mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" \
--all-databases --single-transaction | \
gzip > backups/all_mysql_$(date +%Y%m%d_%H%M%S).sql.gz
# Restore
gunzip -c backups/mydb_20250208_120000.sql.gz | \
docker exec -i my_mysql mysql -u root -p"${MYSQL_ROOT_PASSWORD}" mydb
The --single-transaction flag is essential for InnoDB tables, as it creates a consistent snapshot without locking the database.
MongoDB
# Dump MongoDB
docker exec my_mongo mongodump --archive --gzip \
--db mydb > backups/mongo_$(date +%Y%m%d_%H%M%S).gz
# Restore
docker exec -i my_mongo mongorestore --archive --gzip \
--db mydb < backups/mongo_20250208_120000.gz
Redis
# Trigger an RDB snapshot
docker exec my_redis redis-cli BGSAVE
# Copy the dump file
docker cp my_redis:/data/dump.rdb ./backups/redis_$(date +%Y%m%d_%H%M%S).rdb
# Alternatively, use redis-cli to save to stdout
docker exec my_redis redis-cli --rdb - > backups/redis_dump.rdb
Automated Backup Script
Here is a comprehensive backup script that handles volumes, databases, and configurations:
#!/bin/bash
# docker-backup.sh - Comprehensive Docker backup script
set -euo pipefail
BACKUP_DIR="/backups/docker/$(date +%Y%m%d_%H%M%S)"
RETENTION_DAYS=30
REMOTE_DEST="s3://my-backups/docker/"
mkdir -p "$BACKUP_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Backup all named volumes
backup_volumes() {
log "Backing up Docker volumes..."
for volume in $(docker volume ls -q); do
log " Volume: $volume"
docker run --rm \
-v "$volume":/source:ro \
-v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/volume_${volume}.tar.gz" -C /source .
done
}
# Backup database containers
backup_databases() {
log "Backing up databases..."
# PostgreSQL containers
for container in $(docker ps --filter "ancestor=postgres" -q); do
name=$(docker inspect --format '{{.Name}}' "$container" | sed 's/\///')
log " PostgreSQL: $name"
docker exec "$container" pg_dumpall -U postgres | \
gzip > "$BACKUP_DIR/db_pg_${name}.sql.gz"
done
# MySQL/MariaDB containers
for container in $(docker ps --filter "ancestor=mysql" -q \
&& docker ps --filter "ancestor=mariadb" -q); do
name=$(docker inspect --format '{{.Name}}' "$container" | sed 's/\///')
log " MySQL: $name"
docker exec "$container" mysqldump --all-databases \
--single-transaction -u root -p"$MYSQL_ROOT_PASSWORD" | \
gzip > "$BACKUP_DIR/db_mysql_${name}.sql.gz"
done
}
# Backup Compose files and configs
backup_configs() {
log "Backing up configurations..."
# Export container inspect data
for container in $(docker ps -aq); do
name=$(docker inspect --format '{{.Name}}' "$container" | sed 's/\///')
docker inspect "$container" > "$BACKUP_DIR/inspect_${name}.json"
done
# Copy Compose files
find /opt/docker /home/*/docker -name "docker-compose*.yml" \
-exec cp --parents {} "$BACKUP_DIR/compose/" \; 2>/dev/null || true
}
# Upload to remote storage
upload_remote() {
if command -v aws &> /dev/null; then
log "Uploading to S3..."
aws s3 sync "$BACKUP_DIR" "$REMOTE_DEST$(basename $BACKUP_DIR)/"
fi
}
# Clean old backups
cleanup() {
log "Cleaning backups older than ${RETENTION_DAYS} days..."
find /backups/docker -maxdepth 1 -type d \
-mtime +${RETENTION_DAYS} -exec rm -rf {} \;
}
# Run all backup tasks
backup_volumes
backup_databases
backup_configs
upload_remote
cleanup
log "Backup completed: $BACKUP_DIR"
log "Total size: $(du -sh $BACKUP_DIR | cut -f1)"
Schedule this with cron:
# Run daily at 2 AM
0 2 * * * /opt/scripts/docker-backup.sh >> /var/log/docker-backup.log 2>&1
Backup with Docker Compose Sidecar
You can integrate backups directly into your Docker Compose stack using a backup sidecar container:
version: "3.8"
services:
postgres:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
backup:
image: prodrigestivill/postgres-backup-local
restart: always
volumes:
- ./backups:/backups
depends_on:
- postgres
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: mydb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
SCHEDULE: "@daily"
BACKUP_KEEP_DAYS: 7
BACKUP_KEEP_WEEKS: 4
BACKUP_KEEP_MONTHS: 6
volumes:
pgdata:
Restore Testing: The Most Important Step
A backup is worthless if you cannot restore from it. Schedule regular restore tests:
#!/bin/bash
# test-restore.sh - Verify backup integrity
set -euo pipefail
LATEST_BACKUP=$(ls -td /backups/docker/*/ | head -1)
log() { echo "[$(date '+%H:%M:%S')] $1"; }
# Test volume restore
test_volume_restore() {
local backup_file="$1"
local test_volume="restore_test_$(date +%s)"
docker volume create "$test_volume"
docker run --rm \
-v "$test_volume":/target \
-v "$LATEST_BACKUP":/backup:ro \
alpine sh -c "cd /target && tar xzf /backup/$backup_file"
# Verify files exist
local file_count=$(docker run --rm -v "$test_volume":/data \
alpine find /data -type f | wc -l)
docker volume rm "$test_volume"
if [ "$file_count" -gt 0 ]; then
log "PASS: $backup_file ($file_count files restored)"
else
log "FAIL: $backup_file (no files found)"
return 1
fi
}
# Test database restore
test_db_restore() {
local backup_file="$1"
docker run --rm -d --name restore_test_pg \
-e POSTGRES_PASSWORD=testpass postgres:16
sleep 5 # Wait for PostgreSQL to start
gunzip -c "$LATEST_BACKUP/$backup_file" | \
docker exec -i restore_test_pg psql -U postgres
local table_count=$(docker exec restore_test_pg \
psql -U postgres -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public'")
docker rm -f restore_test_pg
log "DB restore test: $table_count tables restored from $backup_file"
}
# Run tests
for vol_backup in "$LATEST_BACKUP"/volume_*.tar.gz; do
test_volume_restore "$(basename $vol_backup)"
done
for db_backup in "$LATEST_BACKUP"/db_pg_*.sql.gz; do
test_db_restore "$(basename $db_backup)"
done
Remote Backup Destinations
Local backups protect against accidental deletion but not against hardware failure. Always maintain at least one off-site copy:
S3-Compatible Storage
# Using rclone for versatile remote backup
rclone sync /backups/docker remote:docker-backups \
--transfers 4 \
--checkers 8 \
--bwlimit 50M
# Using AWS CLI directly
aws s3 sync /backups/docker s3://my-backups/docker/ \
--storage-class STANDARD_IA \
--delete
Restic for Deduplication
# Initialize a restic repository
restic init --repo s3:s3.amazonaws.com/my-backups/restic
# Backup with deduplication
restic backup /backups/docker \
--repo s3:s3.amazonaws.com/my-backups/restic \
--tag docker-volumes
# Restic automatically deduplicates, saving significant storage
restic snapshots --repo s3:s3.amazonaws.com/my-backups/restic
Backup Strategy Summary
A robust Docker backup strategy follows the 3-2-1 rule: three copies of your data, on two different types of media, with one copy off-site.
- Primary: Live data in Docker volumes on your server
- Local backup: Daily dumps and volume archives on a separate disk or partition
- Remote backup: Encrypted copies in S3, B2, or another cloud storage provider
Combine this with infrastructure-as-code practices: keep all Compose files, environment templates, and deployment scripts in version control. Your goal should be the ability to recreate your entire Docker infrastructure on fresh hardware using only your Git repository and your latest backup archive.
With platforms like usulnet, you gain visibility into which containers have volumes, making it easier to audit your backup coverage and ensure nothing falls through the cracks.