Files
linux-scripts/docker-volume-backup.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

270 lines
8.8 KiB
Bash

#!/bin/bash
#############################################################
#### Docker Volume Backup Script ####
#### Backup and restore Docker named volumes using ####
#### tar archives with optional compression ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version: 1.0 ####
#### ####
#### Usage: ./docker-volume-backup.sh [OPTIONS] ####
#############################################################
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME
readonly DEFAULT_BACKUP_DIR="/opt/docker-backups"
readonly DEFAULT_RETAIN=7
readonly ALPINE_IMAGE="alpine:latest"
BACKUP_DIR="$DEFAULT_BACKUP_DIR"
RETAIN="$DEFAULT_RETAIN"
MODE=""
TARGET_VOLUME=""
RESTORE_ARCHIVE=""
# Colors
readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
log_step() { echo -e "${BLUE}[STEP]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
show_help() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Backup and restore Docker named volumes using tar archives with compression.
OPTIONS:
--backup [VOLUME] Backup all named volumes, or a specific volume if given
--restore ARCHIVE Restore a volume from the specified tar.gz archive
--list List available backups
--backup-dir PATH Backup directory (default: $DEFAULT_BACKUP_DIR)
--retain N Number of backups to keep per volume (default: $DEFAULT_RETAIN)
--help, -h Show this help message
EXAMPLES:
$SCRIPT_NAME --backup
$SCRIPT_NAME --backup my_volume
$SCRIPT_NAME --restore $DEFAULT_BACKUP_DIR/my_volume_20260309_143022.tar.gz
$SCRIPT_NAME --list
$SCRIPT_NAME --backup --backup-dir /mnt/backups --retain 14
EOF
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--backup)
MODE="backup"; shift
[[ $# -gt 0 && ! "$1" =~ ^-- ]] && { TARGET_VOLUME="$1"; shift; }
;;
--restore)
MODE="restore"
[[ $# -lt 2 ]] && { log_error "--restore requires an archive path"; exit 1; }
RESTORE_ARCHIVE="$2"; shift 2
;;
--list) MODE="list"; shift ;;
--backup-dir)
[[ $# -lt 2 ]] && { log_error "--backup-dir requires a path"; exit 1; }
BACKUP_DIR="$2"; shift 2
;;
--retain)
[[ $# -lt 2 ]] && { log_error "--retain requires a number"; exit 1; }
RETAIN="$2"; shift 2
;;
--help|-h) show_help ;;
*) log_error "Unknown option: $1"; show_help ;;
esac
done
if [[ -z "$MODE" ]]; then
log_error "No action specified. Use --backup, --restore, or --list."
show_help
fi
}
check_dependencies() {
if ! command -v docker &>/dev/null; then
log_error "docker is required but not installed"; exit 1
fi
if ! docker info &>/dev/null; then
log_error "Cannot connect to Docker daemon. Is it running?"; exit 1
fi
}
backup_volume() {
local volume_name="$1"
local timestamp archive_name final_path tmp_file size
timestamp=$(date +%Y%m%d_%H%M%S)
archive_name="${volume_name}_${timestamp}.tar.gz"
final_path="${BACKUP_DIR}/${archive_name}"
log_step "Backing up volume: ${volume_name}"
if ! docker volume inspect "$volume_name" &>/dev/null; then
log_error "Volume '$volume_name' does not exist"; return 1
fi
mkdir -p "$BACKUP_DIR"
tmp_file=$(mktemp "${BACKUP_DIR}/.backup_XXXXXX.tar.gz")
if docker run --rm \
-v "${volume_name}:/source:ro" \
-v "${BACKUP_DIR}:/backup" \
"$ALPINE_IMAGE" \
tar czf "/backup/$(basename "$tmp_file")" -C /source . 2>/dev/null; then
mv "$tmp_file" "$final_path"
size=$(du -h "$final_path" | cut -f1)
log_info "Created backup: ${final_path} (${size})"
else
rm -f "$tmp_file"
log_error "Failed to backup volume: ${volume_name}"; return 1
fi
}
do_backup() {
log_step "Starting Docker volume backup"
log_info "Backup directory: ${BACKUP_DIR}"
local volumes=()
if [[ -n "$TARGET_VOLUME" ]]; then
volumes=("$TARGET_VOLUME")
else
while IFS= read -r vol; do
[[ -n "$vol" ]] && volumes+=("$vol")
done < <(docker volume ls --format '{{.Name}}' | sort)
fi
if [[ ${#volumes[@]} -eq 0 ]]; then
log_warn "No Docker named volumes found"; return 0
fi
log_info "Found ${#volumes[@]} volume(s) to backup"
local success=0 failed=0
for vol in "${volumes[@]}"; do
if backup_volume "$vol"; then
success=$((success + 1))
else
failed=$((failed + 1))
fi
done
apply_retention
log_step "Backup complete: ${success} succeeded, ${failed} failed"
[[ $failed -gt 0 ]] && return 1
return 0
}
do_restore() {
if [[ ! -f "$RESTORE_ARCHIVE" ]]; then
log_error "Archive not found: ${RESTORE_ARCHIVE}"; exit 1
fi
local basename_archive volume_name
basename_archive=$(basename "$RESTORE_ARCHIVE")
volume_name=$(echo "$basename_archive" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
if [[ -z "$volume_name" || "$volume_name" == "$basename_archive" ]]; then
log_error "Cannot determine volume name from archive: ${basename_archive}"
log_error "Expected format: volumename_YYYYMMDD_HHMMSS.tar.gz"; exit 1
fi
log_step "Restoring volume: ${volume_name} from ${RESTORE_ARCHIVE}"
if ! docker volume inspect "$volume_name" &>/dev/null; then
log_info "Creating volume: ${volume_name}"
docker volume create "$volume_name" >/dev/null
else
log_warn "Volume '${volume_name}' already exists — contents will be overwritten"
fi
local archive_abs archive_dir archive_file
archive_abs=$(realpath "$RESTORE_ARCHIVE")
archive_dir=$(dirname "$archive_abs")
archive_file=$(basename "$archive_abs")
if docker run --rm \
-v "${volume_name}:/target" \
-v "${archive_dir}:/backup:ro" \
"$ALPINE_IMAGE" \
sh -c "rm -rf /target/* /target/..?* /target/.[!.]* 2>/dev/null; tar xzf /backup/${archive_file} -C /target" 2>/dev/null; then
log_info "Volume '${volume_name}' restored successfully"
else
log_error "Failed to restore volume: ${volume_name}"; exit 1
fi
}
do_list() {
if [[ ! -d "$BACKUP_DIR" ]]; then
log_info "No backups found (directory does not exist: ${BACKUP_DIR})"; return 0
fi
local count=0
log_step "Available backups in ${BACKUP_DIR}:"
printf "\n %-40s %-10s %s\n" "ARCHIVE" "SIZE" "VOLUME"
printf " %-40s %-10s %s\n" "-------" "----" "------"
for archive in "$BACKUP_DIR"/*.tar.gz; do
[[ -f "$archive" ]] || continue
count=$((count + 1))
local name size vol_name
name=$(basename "$archive")
size=$(du -h "$archive" | cut -f1)
vol_name=$(echo "$name" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
printf " %-40s %-10s %s\n" "$name" "$size" "$vol_name"
done
echo ""
if [[ $count -eq 0 ]]; then
log_info "No backup archives found in ${BACKUP_DIR}"
else
log_info "${count} backup(s) found"
fi
}
apply_retention() {
log_step "Applying retention policy (keep last ${RETAIN} per volume)"
local vol_names=()
for archive in "$BACKUP_DIR"/*.tar.gz; do
[[ -f "$archive" ]] || continue
local name vol_name
name=$(basename "$archive")
vol_name=$(echo "$name" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
vol_names+=("$vol_name")
done
local unique_vols
unique_vols=$(printf '%s\n' "${vol_names[@]}" 2>/dev/null | sort -u)
while IFS= read -r vol; do
[[ -z "$vol" ]] && continue
local old_archives=()
while IFS= read -r f; do
[[ -n "$f" ]] && old_archives+=("$f")
done < <(ls -1t "$BACKUP_DIR"/${vol}_[0-9]*_[0-9]*.tar.gz 2>/dev/null | tail -n +$((RETAIN + 1)))
for old_archive in "${old_archives[@]}"; do
log_info "Removing old backup: $(basename "$old_archive")"
rm -f "$old_archive"
done
done <<< "$unique_vols"
}
main() {
parse_args "$@"
check_dependencies
case "$MODE" in
backup) do_backup ;;
restore) do_restore ;;
list) do_list ;;
esac
}
main "$@"