#!/bin/bash """ QR Code Manager - Docker Backup Script This script provides backup and restore functionality for dockerized QR Code Manager. It handles both data volumes and complete application backups. Usage: ./docker_backup.sh [command] [options] Commands: backup-data - Backup data volumes only backup-full - Full backup including application and data restore - Restore from backup list - List available backups cleanup - Remove old backups schedule - Set up automated backups Examples: ./docker_backup.sh backup-data ./docker_backup.sh backup-full ./docker_backup.sh restore backup_20240801_120000.tar.gz ./docker_backup.sh list """ set -e # Exit on any error # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKUP_DIR="${SCRIPT_DIR}/backups" CONTAINER_NAME="qr-code-manager" COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper functions log_info() { echo -e "${BLUE}ℹ️ $1${NC}" } log_success() { echo -e "${GREEN}✅ $1${NC}" } log_warning() { echo -e "${YELLOW}⚠️ $1${NC}" } log_error() { echo -e "${RED}❌ $1${NC}" } # Ensure backup directory exists mkdir -p "${BACKUP_DIR}" # Check if Docker and docker-compose are available check_dependencies() { if ! command -v docker &> /dev/null; then log_error "Docker is not installed or not in PATH" exit 1 fi if command -v docker &> /dev/null && docker compose version &> /dev/null 2>&1; then # Use docker compose (newer syntax) COMPOSE_CMD="docker compose" elif command -v docker-compose &> /dev/null; then # Fallback to docker-compose (older syntax) COMPOSE_CMD="docker-compose" else log_error "Neither 'docker compose' nor 'docker-compose' is available" exit 1 fi } # Get timestamp for backup naming get_timestamp() { date +"%Y%m%d_%H%M%S" } # Check if container is running is_container_running() { docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$" } # Backup data volumes only backup_data() { local timestamp=$(get_timestamp) local backup_name="qr_data_backup_${timestamp}.tar.gz" local backup_path="${BACKUP_DIR}/${backup_name}" log_info "Creating data backup: ${backup_name}" if is_container_running; then log_info "Container is running, creating hot backup..." # Create backup from running container docker exec "${CONTAINER_NAME}" tar czf /tmp/data_backup.tar.gz \ -C /app data/ || { log_error "Failed to create backup inside container" return 1 } # Copy backup from container to host docker cp "${CONTAINER_NAME}:/tmp/data_backup.tar.gz" "${backup_path}" || { log_error "Failed to copy backup from container" return 1 } # Cleanup temp file in container docker exec "${CONTAINER_NAME}" rm -f /tmp/data_backup.tar.gz else log_info "Container is not running, creating backup from volumes..." # Create backup using temporary container docker run --rm \ -v "${SCRIPT_DIR}/data:/source/data:ro" \ -v "${BACKUP_DIR}:/backup" \ alpine:latest tar czf "/backup/${backup_name}" -C /source data/ || { log_error "Failed to create backup from volumes" return 1 } fi # Add metadata local metadata_file="${BACKUP_DIR}/${backup_name%.tar.gz}.json" cat > "${metadata_file}" << EOF { "backup_type": "data", "timestamp": "$(date -Iseconds)", "container_name": "${CONTAINER_NAME}", "backup_method": "$(if is_container_running; then echo 'hot'; else echo 'cold'; fi)", "backup_size": "$(stat -f%z "${backup_path}" 2>/dev/null || stat -c%s "${backup_path}" 2>/dev/null || echo 'unknown')" } EOF log_success "Data backup created: ${backup_path}" log_info "Backup size: $(du -h "${backup_path}" | cut -f1)" } # Full backup including application backup_full() { local timestamp=$(get_timestamp) local backup_name="qr_full_backup_${timestamp}.tar.gz" local backup_path="${BACKUP_DIR}/${backup_name}" log_info "Creating full backup: ${backup_name}" # Stop container for consistent backup local was_running=false if is_container_running; then log_warning "Stopping container for consistent backup..." ${COMPOSE_CMD} -f "${COMPOSE_FILE}" down was_running=true fi # Create full backup tar czf "${backup_path}" \ --exclude="backups" \ --exclude=".git" \ --exclude="__pycache__" \ --exclude="*.pyc" \ --exclude="*.log" \ -C "${SCRIPT_DIR}/.." \ "$(basename "${SCRIPT_DIR}")" || { log_error "Failed to create full backup" return 1 } # Restart container if it was running if [ "$was_running" = true ]; then log_info "Restarting container..." ${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d fi # Add metadata local metadata_file="${BACKUP_DIR}/${backup_name%.tar.gz}.json" cat > "${metadata_file}" << EOF { "backup_type": "full", "timestamp": "$(date -Iseconds)", "container_name": "${CONTAINER_NAME}", "backup_method": "cold", "backup_size": "$(stat -f%z "${backup_path}" 2>/dev/null || stat -c%s "${backup_path}" 2>/dev/null || echo 'unknown')" } EOF log_success "Full backup created: ${backup_path}" log_info "Backup size: $(du -h "${backup_path}" | cut -f1)" } # List available backups list_backups() { log_info "Available backups:" echo "$(printf '%s' '─%.0s' {1..80})" if [ ! "$(ls -A "${BACKUP_DIR}"/*.tar.gz 2>/dev/null)" ]; then echo "No backups found" return fi for backup_file in "${BACKUP_DIR}"/*.tar.gz; do if [ -f "$backup_file" ]; then local basename=$(basename "$backup_file") local size=$(du -h "$backup_file" | cut -f1) local date=$(stat -f%Sm -t "%Y-%m-%d %H:%M" "$backup_file" 2>/dev/null || \ stat -c%y "$backup_file" 2>/dev/null | cut -d' ' -f1-2) # Determine backup type local type="Unknown" if [[ "$basename" == *"data"* ]]; then type="📄 Data" elif [[ "$basename" == *"full"* ]]; then type="📦 Full" fi printf " %-12s | %-40s | %8s | %s\n" "$type" "$basename" "$size" "$date" fi done } # Restore from backup restore_backup() { local backup_file="$1" if [ -z "$backup_file" ]; then log_error "Please specify backup file to restore" echo "Available backups:" list_backups return 1 fi # Check if backup file exists local backup_path="${backup_file}" if [ ! -f "$backup_path" ]; then backup_path="${BACKUP_DIR}/${backup_file}" if [ ! -f "$backup_path" ]; then log_error "Backup file not found: $backup_file" return 1 fi fi log_warning "Restoring from backup: $(basename "$backup_path")" # Confirm restoration echo -n "This will overwrite current data. Continue? (y/N): " read -r confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then log_info "Restoration cancelled" return 0 fi # Stop container if is_container_running; then log_info "Stopping container..." ${COMPOSE_CMD} -f "${COMPOSE_FILE}" down fi # Determine backup type and restore accordingly if [[ "$(basename "$backup_path")" == *"data"* ]]; then log_info "Restoring data backup..." # Backup current data if [ -d "${SCRIPT_DIR}/data" ]; then mv "${SCRIPT_DIR}/data" "${SCRIPT_DIR}/data.backup.$(get_timestamp)" fi # Extract data backup tar xzf "$backup_path" -C "${SCRIPT_DIR}" || { log_error "Failed to extract data backup" return 1 } elif [[ "$(basename "$backup_path")" == *"full"* ]]; then log_info "Restoring full backup..." # Create restoration directory local restore_dir="${SCRIPT_DIR}/../qr-restore-$(get_timestamp)" mkdir -p "$restore_dir" # Extract full backup tar xzf "$backup_path" -C "$restore_dir" || { log_error "Failed to extract full backup" return 1 } log_warning "Full backup extracted to: $restore_dir" log_warning "Please manually review and copy files as needed" return 0 fi # Restart container log_info "Starting container..." ${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d log_success "Restoration completed successfully" } # Cleanup old backups cleanup_backups() { local keep_count=${1:-10} log_info "Cleaning up old backups (keeping $keep_count most recent)" # Get list of backup files sorted by modification time (newest first) local backup_files=($(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null || true)) if [ ${#backup_files[@]} -le $keep_count ]; then log_info "No cleanup needed (${#backup_files[@]} backups found)" return fi # Remove old backups for ((i=$keep_count; i<${#backup_files[@]}; i++)); do local backup_file="${backup_files[$i]}" local metadata_file="${backup_file%.tar.gz}.json" log_info "Removing old backup: $(basename "$backup_file")" rm -f "$backup_file" "$metadata_file" done log_success "Cleanup completed" } # Set up automated backups schedule_backups() { log_info "Setting up automated backups with cron..." # Create backup script for cron local cron_script="${SCRIPT_DIR}/automated_backup.sh" cat > "$cron_script" << 'EOF' #!/bin/bash # Automated backup script for QR Code Manager cd "$(dirname "$0")" ./docker_backup.sh backup-data if [ $(date +%u) -eq 1 ]; then # Monday ./docker_backup.sh backup-full fi ./docker_backup.sh cleanup 15 EOF chmod +x "$cron_script" # Suggest cron entries echo log_info "Automated backup script created: $cron_script" echo echo "Add this to your crontab (crontab -e) for daily backups:" echo "# QR Code Manager daily backup at 2 AM" echo "0 2 * * * $cron_script >> ${SCRIPT_DIR}/backup.log 2>&1" echo echo "Or for hourly data backups:" echo "# QR Code Manager hourly data backup" echo "0 * * * * ${SCRIPT_DIR}/docker_backup.sh backup-data >> ${SCRIPT_DIR}/backup.log 2>&1" } # Main script logic main() { check_dependencies case "${1:-}" in "backup-data") backup_data ;; "backup-full") backup_full ;; "restore") restore_backup "$2" ;; "list") list_backups ;; "cleanup") cleanup_backups "$2" ;; "schedule") schedule_backups ;; *) echo "QR Code Manager - Docker Backup Script" echo echo "Usage: $0 [command] [options]" echo echo "Commands:" echo " backup-data - Backup data volumes only" echo " backup-full - Full backup including application and data" echo " restore - Restore from backup" echo " list - List available backups" echo " cleanup [n] - Remove old backups (keep n most recent, default: 10)" echo " schedule - Set up automated backups" echo echo "Examples:" echo " $0 backup-data" echo " $0 backup-full" echo " $0 restore qr_data_backup_20240801_120000.tar.gz" echo " $0 list" echo " $0 cleanup 5" ;; esac } main "$@"