Skip to content

Instantly share code, notes, and snippets.

@dfederm
Created March 25, 2026 03:46
Show Gist options
  • Select an option

  • Save dfederm/cb11bc9281d2af602d6058bceca0920e to your computer and use it in GitHub Desktop.

Select an option

Save dfederm/cb11bc9281d2af602d6058bceca0920e to your computer and use it in GitHub Desktop.
Backup and Restore Docker volumes
#!/bin/bash
# Back up Docker named volumes before a migration.
#
# Named volumes live on the container's local filesystem, NOT on your ZFS pool,
# so they won't survive a migration unless explicitly backed up.
#
# Usage:
# ./backup-docker-volumes.sh <backup-dir> [volume1] [volume2] ...
#
# Example:
# ./backup-docker-volumes.sh /mnt/pool/backups immich_database-data uptime-kuma_data
#
# If no volumes are specified, all named volumes are backed up.
set -euo pipefail
BACKUP_DIR="$(realpath "${1:?Usage: $0 <backup-dir> [volume1] [volume2] ...}")"
shift
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
backup_volume() {
local volume="$1"
local archive="${BACKUP_DIR}/${volume}_${TIMESTAMP}.tar.gz"
if ! docker volume inspect "$volume" &>/dev/null; then
log "WARNING: Volume '$volume' does not exist, skipping"
return
fi
log "Backing up $volume..."
docker run --rm \
-v "${volume}:/source:ro" \
-v "${BACKUP_DIR}:/backup" \
alpine tar czf "/backup/${volume}_${TIMESTAMP}.tar.gz" -C /source .
log " -> $archive"
}
mkdir -p "$BACKUP_DIR"
# If specific volumes were provided, back up only those.
# Otherwise, back up all named volumes.
if [ $# -gt 0 ]; then
VOLUMES=("$@")
else
mapfile -t VOLUMES < <(docker volume ls --quiet --filter "dangling=false")
fi
for volume in "${VOLUMES[@]}"; do
backup_volume "$volume"
done
log "=== Volume backup complete ==="
log "Backups at: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"/*_${TIMESTAMP}.tar.gz 2>/dev/null
#!/bin/bash
# Restore Docker named volumes from backups created by backup-docker-volumes.sh.
# Run AFTER migration to restore data that lived on the old container's local filesystem.
#
# Usage:
# ./restore-docker-volumes.sh <backup-dir> [volume1] [volume2] ...
#
# Example:
# ./restore-docker-volumes.sh /mnt/pool/backups immich_database-data uptime-kuma_data
#
# If no volumes are specified, all backups in the directory are restored.
# The most recent backup for each volume is used automatically.
set -euo pipefail
BACKUP_DIR="$(realpath "${1:?Usage: $0 <backup-dir> [volume1] [volume2] ...}")"
shift
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
restore_volume() {
local volume="$1"
# Find the most recent backup for this volume
local archive
archive=$(ls -t "$BACKUP_DIR"/${volume}_*.tar.gz 2>/dev/null | head -1)
if [ -z "$archive" ] || [ ! -f "$archive" ]; then
log "WARNING: No backup found for '$volume', skipping"
return
fi
log "Restoring $volume from $(basename "$archive")..."
# Create the volume if it doesn't exist
docker volume create "$volume" &>/dev/null || true
docker run --rm \
-v "${volume}:/target" \
-v "${BACKUP_DIR}:/backup:ro" \
alpine sh -c "find /target -mindepth 1 -delete && tar xzf /backup/$(basename "$archive") -C /target"
log " $volume restored"
}
if [ ! -d "$BACKUP_DIR" ]; then
echo "ERROR: Backup directory not found: $BACKUP_DIR" >&2
exit 1
fi
# If specific volumes were provided, restore only those.
# Otherwise, discover volumes from backup filenames.
if [ $# -gt 0 ]; then
VOLUMES=("$@")
else
mapfile -t VOLUMES < <(
ls "$BACKUP_DIR"/*_*.tar.gz 2>/dev/null \
| xargs -n1 basename \
| sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//' \
| sort -u
)
fi
if [ ${#VOLUMES[@]} -eq 0 ]; then
log "No volumes found to restore"
exit 0
fi
for volume in "${VOLUMES[@]}"; do
restore_volume "$volume"
done
log "=== Volume restore complete ==="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment