Mac Workstation Backup¶
This documentation covers the Restic-based backup strategy for macOS workstations.
Purpose¶
This script encrypts and uploads the Mac's home directory to the Hetzner Storage Box. It is useful for full machine backups before wiping or migrating.
Script Source¶
#!/bin/bash
#
# Mac FULL Encrypted Backup Script
# Backs up EVERYTHING except cloud-synced folders
# Uses restic with AES-256 encryption
#
set -e
# Ensure Homebrew and binaries are in PATH
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
# ============================================
# CONFIGURATION
# ============================================
# Restic and Rclone binary locations
RESTIC="$HOME/bin/restic"
RCLONE="/opt/homebrew/bin/rclone"
# Storage Box configuration (using rclone backend for speed)
REPO="rclone:storagebox:mac-backup"
# Password file (IMPORTANT: Create this file with your encryption password)
PASSWORD_FILE="$HOME/.config/restic/password"
# Backup your entire home folder
BACKUP_PATHS="$HOME"
# ONLY exclude cloud-synced folders (everything else is backed up!)
EXCLUDE_PATTERNS=(
# Cloud synced (already backed up elsewhere)
"Library/Mobile Documents" # iCloud Drive
"Library/CloudStorage" # OneDrive, Google Drive, Dropbox mounted here
"Dropbox"
"Google Drive"
"OneDrive"
# macOS system stuff that can't be backed up properly
".Trash"
".DS_Store"
# Stuff that would cause errors
"*.sock"
"*.pid"
)
# Log file
LOG_FILE="$HOME/mac-backup.log"
# ============================================
# COLORS
# ============================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
msg="[$(date +%H:%M:%S)] $1"
echo -e "${GREEN}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
}
warn() {
msg="[$(date +%H:%M:%S)] WARNING: $1"
echo -e "${YELLOW}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
}
error() {
msg="[$(date +%H:%M:%S)] ERROR: $1"
echo -e "${RED}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
exit 1
}
# ============================================
# SETUP
# ============================================
# Check if password file exists
if [ ! -f "$PASSWORD_FILE" ]; then
echo -e "${RED}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ ERROR: Password file not found! ║${NC}"
echo -e "${RED}╠════════════════════════════════════════════════════════════════╣${NC}"
echo -e "${RED}║ Create your encryption password: ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ mkdir -p ~/.config/restic ║${NC}"
echo -e "${RED}║ echo 'YOUR_STRONG_PASSWORD' > ~/.config/restic/password ║${NC}"
echo -e "${RED}║ chmod 600 ~/.config/restic/password ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ ⚠️ SAVE THIS PASSWORD SECURELY - Cannot recover without it! ║${NC}"
echo -e "${RED}╚════════════════════════════════════════════════════════════════╝${NC}"
exit 1
fi
# Export for restic
export RESTIC_PASSWORD_FILE="$PASSWORD_FILE"
export RESTIC_REPOSITORY="$REPO"
# Build exclude arguments
EXCLUDE_ARGS=""
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude '$pattern'"
done
# ============================================
# COMMANDS
# ============================================
case "$1" in
init)
log "Initializing encrypted repository..."
log "This will create a new encrypted backup repository at: $REPO"
$RESTIC init
log "Repository initialized! You can now run: $0 backup"
;;
backup)
log "Starting PRIORITIZED encrypted backup..."
log "Repository: $REPO"
log "Log file: $LOG_FILE"
# Priority 1: Most important folders first
log "=== Phase 1: Critical folders (Desktop, Documents, Downloads) ==="
PRIORITY_FOLDERS=(
"$HOME/Desktop"
"$HOME/Documents"
"$HOME/Downloads"
)
for folder in "${PRIORITY_FOLDERS[@]}"; do
if [ -d "$folder" ]; then
log "Backing up: $folder"
eval "$RESTIC backup \
--verbose \
--pack-size 128 \
--option rclone.program=\"$RCLONE\" \
--option rclone.args=\"serve restic --stdio --sftp-concurrency 8\" \
$EXCLUDE_ARGS \
'$folder'" 2>&1 | tee -a "$LOG_FILE"
fi
done
log "=== Phase 2: Full home backup (remaining files) ==="
# Run full backup (restic will skip already-backed-up files)
eval "$RESTIC backup \
--verbose \
--pack-size 128 \
--option rclone.program=\"$RCLONE\" \
--option rclone.args=\"serve restic --stdio --sftp-concurrency 8\" \
$EXCLUDE_ARGS \
'$HOME'" 2>&1 | tee -a "$LOG_FILE"
log "Backup completed!"
# Show stats
log "Repository stats:"
$RESTIC stats | tee -a "$LOG_FILE"
# Desktop notification
osascript -e 'display notification "Mac backup completed!" with title "Backup Done" sound name "Glass"' 2>/dev/null || true
;;
backup-background)
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Starting backup in background with sleep prevention ║${NC}"
echo -e "${GREEN}╠════════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}║ • Mac will NOT sleep during backup ║${NC}"
echo -e "${GREEN}║ • You can close this terminal ║${NC}"
echo -e "${GREEN}║ • You can close the lid (will keep running!) ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Monitor progress: ║${NC}"
echo -e "${GREEN}║ tail -f ~/mac-backup.log ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ You'll get a notification when done! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
# Run in background with caffeinate (prevents sleep)
nohup caffeinate -i "$0" backup >> "$LOG_FILE" 2>&1 &
PID=$!
echo "$PID" > "$HOME/.mac-backup.pid"
log "Backup started in background (PID: $PID)"
log "Monitor: tail -f ~/mac-backup.log"
;;
status)
if [ -f "$HOME/.mac-backup.pid" ]; then
PID=$(cat "$HOME/.mac-backup.pid")
if ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}Backup is running (PID: $PID)${NC}"
echo "Last 10 lines of log:"
tail -10 "$LOG_FILE"
else
echo -e "${YELLOW}No backup currently running${NC}"
echo "Last backup ended. Check log: tail -50 ~/mac-backup.log"
rm -f "$HOME/.mac-backup.pid"
fi
else
echo -e "${YELLOW}No backup currently running${NC}"
fi
;;
stop)
if [ -f "$HOME/.mac-backup.pid" ]; then
PID=$(cat "$HOME/.mac-backup.pid")
if ps -p $PID > /dev/null 2>&1; then
kill $PID
log "Backup stopped (PID: $PID)"
rm -f "$HOME/.mac-backup.pid"
else
echo "Backup not running"
rm -f "$HOME/.mac-backup.pid"
fi
else
echo "No backup running"
fi
;;
snapshots)
log "Listing snapshots..."
$RESTIC snapshots
;;
restore)
if [ -z "$2" ]; then
echo "Usage: $0 restore <snapshot-id> <target-path>"
echo "Example: $0 restore latest /tmp/restore"
exit 1
fi
TARGET="${3:-/tmp/restore}"
log "Restoring snapshot $2 to $TARGET..."
$RESTIC restore "$2" --target "$TARGET"
log "Restore completed to: $TARGET"
;;
prune)
log "Pruning old snapshots..."
log "Keeping: 7 daily, 4 weekly, 6 monthly, 1 yearly"
$RESTIC forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 1 \
--prune
log "Prune completed!"
;;
check)
log "Checking repository integrity..."
$RESTIC check
log "Repository is healthy!"
;;
stats)
$RESTIC stats
$RESTIC snapshots
;;
*)
echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Mac FULL Encrypted Backup - Hetzner Storage Box ║${NC}"
echo -e "${BLUE}╠════════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BLUE}║ Backed up: EVERYTHING in ~/ ║${NC}"
echo -e "${BLUE}║ Excluded: iCloud Drive, Dropbox, Google Drive only ║${NC}"
echo -e "${BLUE}╠════════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BLUE}║ Commands: ║${NC}"
echo -e "${BLUE}║ init - Initialize repo (first time only) ║${NC}"
echo -e "${BLUE}║ backup - Run backup in terminal ║${NC}"
echo -e "${BLUE}║ backup-background- Run in background (survives sleep!) ║${NC}"
echo -e "${BLUE}║ status - Check if backup is running ║${NC}"
echo -e "${BLUE}║ stop - Stop background backup ║${NC}"
echo -e "${BLUE}║ snapshots - List all backups ║${NC}"
echo -e "${BLUE}║ restore - Restore a backup ║${NC}"
echo -e "${BLUE}║ prune - Remove old backups ║${NC}"
echo -e "${BLUE}║ check - Verify integrity ║${NC}"
echo -e "${BLUE}║ stats - Show repository size ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}"
;;
esac