Created
March 25, 2026 03:46
-
-
Save dfederm/cb11bc9281d2af602d6058bceca0920e to your computer and use it in GitHub Desktop.
Backup and Restore Docker volumes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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