📦 Backup and Rotation Recipes
Effective backup and rotation strategies ensure data protection while managing storage efficiently. This recipe provides proven patterns for implementing reliable backup systems with proper retention policies.
🎯 Core Principles
Incremental Backup Strategy
Combine full and incremental backups for efficient storage usage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 | #!/bin/bash
# backup-strategy.sh - Comprehensive backup system
# Configuration
BACKUP_ROOT="/backup"
FULL_BACKUP_DAY="Sunday" # Weekly full backup
RETENTION_DAYS=30
RETENTION_WEEKS=12
RETENTION_MONTHS=12
# Timestamp functions
get_timestamp() {
date +%Y%m%d_%H%M%S
}
get_date_only() {
date +%Y%m%d
}
get_day_of_week() {
date +%A
}
# Backup type determination
should_do_full_backup() {
[ "$(get_day_of_week)" = "$FULL_BACKUP_DAY" ]
}
# Directory structure
create_backup_dirs() {
local backup_base="$1"
mkdir -p "$backup_base"/{daily,weekly,monthly,incoming}
mkdir -p "$backup_base"/logs
# Set proper permissions
chmod 750 "$backup_base"
chown root:backup "$backup_base"
}
# Full backup implementation
perform_full_backup() {
local source_dir="$1"
local backup_base="$2"
local backup_name="$3"
local timestamp=$(get_timestamp)
local backup_path="$backup_base/incoming/${backup_name}_full_${timestamp}.tar.gz"
local log_file="$backup_base/logs/${backup_name}_full_${timestamp}.log"
echo "Starting full backup of $source_dir" | tee "$log_file"
# Create full backup
if tar -czf "$backup_path" -C "$(dirname "$source_dir")" "$(basename "$source_dir")" 2>>"$log_file"; then
echo "Full backup completed: $backup_path" | tee -a "$log_file"
# Move to daily directory
mv "$backup_path" "$backup_base/daily/"
# Create symlink for latest
ln -sf "$(basename "$backup_path")" "$backup_base/latest_full.tar.gz"
return 0
else
echo "Full backup failed" | tee -a "$log_file"
return 1
fi
}
# Incremental backup implementation
perform_incremental_backup() {
local source_dir="$1"
local backup_base="$2"
local backup_name="$3"
local timestamp=$(get_timestamp)
local backup_path="$backup_base/incoming/${backup_name}_incr_${timestamp}.tar.gz"
local log_file="$backup_base/logs/${backup_name}_incr_${timestamp}.log"
# Find last backup for comparison
local last_backup
last_backup=$(find_last_backup "$backup_base")
if [ -z "$last_backup" ]; then
echo "No previous backup found, performing full backup instead" | tee "$log_file"
perform_full_backup "$source_dir" "$backup_base" "$backup_name"
return $?
fi
echo "Starting incremental backup of $source_dir since $last_backup" | tee -a "$log_file"
# Create incremental backup using tar with snapshot file
local snapshot_file="$backup_base/incoming/${backup_name}_snapshot_${timestamp}.snar"
if tar -czf "$backup_path" -g "$snapshot_file" -C "$(dirname "$source_dir")" "$(basename "$source_dir")" 2>>"$log_file"; then
echo "Incremental backup completed: $backup_path" | tee -a "$log_file"
# Move files to appropriate directories
mv "$backup_path" "$backup_base/daily/"
mv "$snapshot_file" "$backup_base/daily/"
# Create symlink for latest
ln -sf "$(basename "$backup_path")" "$backup_base/latest_incr.tar.gz"
return 0
else
echo "Incremental backup failed" | tee -a "$log_file"
rm -f "$snapshot_file" # Cleanup failed snapshot
return 1
fi
}
# Find last backup for incremental comparison
find_last_backup() {
local backup_base="$1"
# Look for most recent snapshot file
find "$backup_base/daily" -name "*.snar" -type f | sort | tail -1
}
|
🔧 Database Backup Patterns
MySQL Backup with Point-in-Time Recovery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 | # mysql-backup.sh - MySQL backup with binary log support
MYSQL_BACKUP_CONFIG="/etc/mysql/backup.conf"
source "$MYSQL_BACKUP_CONFIG"
backup_mysql_database() {
local database_name="$1"
local backup_dir="$2"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$backup_dir/${database_name}_${timestamp}.sql"
local binlog_position_file="$backup_dir/${database_name}_${timestamp}_binlog.pos"
# Create backup directory
mkdir -p "$backup_dir"
# Get master status for point-in-time recovery
local master_status
master_status=$(mysql -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SHOW MASTER STATUS\G" 2>/dev/null)
# Extract binary log position
local binlog_file
local binlog_position
binlog_file=$(echo "$master_status" | grep "File:" | awk '{print $2}')
binlog_position=$(echo "$master_status" | grep "Position:" | awk '{print $2}')
# Save binary log position
echo "Binary Log File: $binlog_file" > "$binlog_position_file"
echo "Binary Log Position: $binlog_position" >> "$binlog_position_file"
# Perform database dump
if mysqldump -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" \
--single-transaction \
--routines \
--triggers \
--events \
"$database_name" > "$backup_file"; then
# Compress backup
gzip "$backup_file"
echo "MySQL backup completed: ${backup_file}.gz"
echo "Binary log position saved: $binlog_position_file"
return 0
else
echo "MySQL backup failed" >&2
rm -f "$backup_file" "$binlog_position_file"
return 1
fi
}
# Point-in-time recovery function
recover_mysql_point_in_time() {
local backup_file="$1"
local target_datetime="$2"
local database_name="$3"
# Restore from backup
gunzip -c "$backup_file" | mysql -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$database_name"
# Apply binary logs from backup position to target time
# This would require additional logic to parse binary logs
echo "Point-in-time recovery initiated to $target_datetime"
}
|
🗃️ File System Backup
Rsync-Based Incremental Backup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 | # rsync-backup.sh - Efficient incremental file backup
perform_rsync_backup() {
local source="$1"
local destination="$2"
local backup_name="$3"
local timestamp=$(get_timestamp)
local backup_path="$destination/$backup_name/$timestamp"
local latest_link="$destination/$backup_name/latest"
local log_file="$destination/logs/${backup_name}_${timestamp}.log"
# Create backup directory
mkdir -p "$backup_path"
mkdir -p "$(dirname "$log_file")"
echo "Starting rsync backup from $source to $backup_path" | tee "$log_file"
# Perform rsync with hard links for efficiency
if rsync -avH --delete \
--link-dest="$latest_link" \
"$source/" "$backup_path/" 2>>"$log_file"; then
echo "Rsync backup completed successfully" | tee -a "$log_file"
# Update latest symlink
rm -f "$latest_link"
ln -s "$timestamp" "$latest_link"
return 0
else
echo "Rsync backup failed" | tee -a "$log_file"
rm -rf "$backup_path" # Cleanup failed backup
return 1
fi
}
# Hard link-based rotation (rsnapshot style)
create_hardlink_rotation() {
local backup_root="$1"
local backup_name="$2"
local max_backups="$3"
# Rotate existing backups
for ((i=max_backups; i>=1; i--)); do
local current="$backup_root/$backup_name.$i"
local next="$backup_root/$backup_name.$((i+1))"
if [ -d "$current" ]; then
if [ $i -eq $max_backups ]; then
# Remove oldest backup
rm -rf "$current"
else
# Rotate backup
mv "$current" "$next"
fi
fi
done
# Create new backup slot
mkdir -p "$backup_root/$backup_name.1"
}
|
🔄 Rotation Policies
Time-Based Retention Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | # retention-manager.sh - Automated backup retention
enforce_retention_policy() {
local backup_base="$1"
local retention_days="$2"
local retention_weeks="$3"
local retention_months="$4"
local current_date
current_date=$(date +%s)
echo "Enforcing retention policy on $backup_base"
# Daily retention (keep last N days)
enforce_daily_retention "$backup_base/daily" "$retention_days"
# Weekly retention (keep one per week)
enforce_weekly_retention "$backup_base/daily" "$retention_weeks"
# Monthly retention (keep one per month)
enforce_monthly_retention "$backup_base/daily" "$retention_months"
}
enforce_daily_retention() {
local backup_dir="$1"
local keep_days="$2"
local cutoff_date
cutoff_date=$(date -d "$keep_days days ago" +%Y%m%d)
echo "Removing daily backups older than $cutoff_date"
find "$backup_dir" -name "*.tar.gz" -type f | while read -r backup_file; do
local file_date
file_date=$(basename "$backup_file" | cut -d_ -f1)
if [ "$file_date" -lt "$cutoff_date" ]; then
echo "Removing old backup: $backup_file"
rm -f "$backup_file"
# Remove associated snapshot files
local snapshot_file
snapshot_file="${backup_file%.tar.gz}.snar"
if [ -f "$snapshot_file" ]; then
rm -f "$snapshot_file"
fi
fi
done
}
enforce_weekly_retention() {
local backup_dir="$1"
local keep_weeks="$2"
echo "Keeping one backup per week for $keep_weeks weeks"
# Group backups by week and keep newest from each week
# Implementation would involve date manipulation and grouping logic
}
enforce_monthly_retention() {
local backup_dir="$1"
local keep_months="$2"
echo "Keeping one backup per month for $keep_months months"
# Similar grouping logic for monthly retention
}
|
🛡️ Backup Verification
Automated Backup Integrity Checking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 | # backup-verifier.sh - Backup integrity validation
verify_backup_integrity() {
local backup_file="$1"
local backup_type="$2" # tar, sql, etc.
case "$backup_type" in
tar)
verify_tar_backup "$backup_file"
;;
sql)
verify_sql_backup "$backup_file"
;;
rsync)
verify_rsync_backup "$backup_file"
;;
*)
echo "Unknown backup type: $backup_type" >&2
return 1
;;
esac
}
verify_tar_backup() {
local tar_file="$1"
echo "Verifying tar backup: $tar_file"
# Check if file exists
if [ ! -f "$tar_file" ]; then
echo "Error: Backup file not found: $tar_file" >&2
return 1
fi
# Verify tar file integrity
if tar -tzf "$tar_file" >/dev/null 2>&1; then
echo "Tar backup integrity check passed"
return 0
else
echo "Error: Tar backup integrity check failed" >&2
return 1
fi
}
verify_sql_backup() {
local sql_file="$1"
echo "Verifying SQL backup: $sql_file"
# Check file existence
if [ ! -f "$sql_file" ]; then
echo "Error: SQL backup file not found: $sql_file" >&2
return 1
fi
# Basic syntax check (MySQL example)
if gunzip -c "$sql_file" | head -100 | grep -E "^(CREATE|INSERT|USE)" >/dev/null; then
echo "SQL backup syntax check passed"
return 0
else
echo "Error: SQL backup syntax check failed" >&2
return 1
fi
}
# Automated verification scheduler
schedule_backup_verification() {
local backup_dir="$1"
local interval_hours="${2:-24}" # Default 24 hours
# Find recent backups to verify
find "$backup_dir" -name "*.tar.gz" -type f -mtime -1 | while read -r backup_file; do
if ! verify_backup_integrity "$backup_file" "tar"; then
echo "Backup verification failed: $backup_file" >&2
# Send alert or notification
fi
done
}
|
🎨 Advanced Backup Features
Encrypted Backup Storage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | # encrypted-backup.sh - Backup with encryption
perform_encrypted_backup() {
local source_dir="$1"
local backup_base="$2"
local backup_name="$3"
local encryption_key_file="$4"
local timestamp=$(get_timestamp)
local plain_backup="$backup_base/incoming/${backup_name}_${timestamp}.tar"
local encrypted_backup="$backup_base/daily/${backup_name}_${timestamp}.tar.gpg"
local log_file="$backup_base/logs/${backup_name}_${timestamp}.log"
# Create plain backup
if tar -cf "$plain_backup" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"; then
echo "Plain backup created: $plain_backup" | tee "$log_file"
# Encrypt backup
if gpg --batch --yes --passphrase-file "$encryption_key_file" \
--symmetric --cipher-algo AES256 \
--output "$encrypted_backup" "$plain_backup"; then
echo "Encrypted backup created: $encrypted_backup" | tee -a "$log_file"
# Remove plain backup
rm -f "$plain_backup"
return 0
else
echo "Encryption failed" | tee -a "$log_file"
rm -f "$plain_backup"
return 1
fi
else
echo "Plain backup failed" | tee "$log_file"
return 1
fi
}
# Decryption function for restores
restore_encrypted_backup() {
local encrypted_file="$1"
local destination="$2"
local decryption_key_file="$3"
local temp_file
temp_file=$(mktemp)
# Decrypt backup
if gpg --batch --yes --passphrase-file "$decryption_key_file" \
--decrypt --output "$temp_file" "$encrypted_file"; then
# Extract backup
if tar -xf "$temp_file" -C "$destination"; then
echo "Backup restored successfully to $destination"
rm -f "$temp_file"
return 0
else
echo "Backup extraction failed" >&2
rm -f "$temp_file"
return 1
fi
else
echo "Decryption failed" >&2
rm -f "$temp_file"
return 1
fi
}
|
🧾 Summary Best Practices
Backup Strategy Guidelines
- Regular Schedule: Implement consistent backup schedules
- Multiple Copies: Follow 3-2-1 rule (3 copies, 2 media, 1 offsite)
- Verification: Regularly test backup integrity
- Retention: Implement appropriate retention policies
- Security: Encrypt sensitive backups
- Monitoring: Monitor backup success/failure
- Documentation: Maintain clear backup procedures
Key Configuration Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | # backup-config-example.sh - Backup configuration template
# Global settings
export BACKUP_ROOT="/backup"
export LOG_DIR="/var/log/backups"
export RETENTION_DAYS=30
export RETENTION_WEEKS=12
export RETENTION_MONTHS=12
# Database settings
export MYSQL_USER="backup_user"
export MYSQL_PASSWORD_FILE="/etc/mysql/backup.password"
export POSTGRES_USER="backup_user"
export POSTGRES_PASSWORD_FILE="/etc/postgresql/backup.password"
# Encryption settings
export ENCRYPTION_KEY_FILE="/etc/backup/encryption.key"
export GPG_RECIPIENT="backup-admin@example.com"
# Notification settings
export ALERT_EMAIL="admin@example.com"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
# Storage settings
export S3_BUCKET="company-backups"
export S3_REGION="us-west-2"
|
🧠 Complete Backup Script Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 | #!/bin/bash
# complete-backup-system.sh - Production-ready backup solution
set -euo pipefail
# Load configuration
CONFIG_FILE="${1:-/etc/backup/config}"
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# Default configuration
: ${BACKUP_ROOT:="/backup"}
: ${SOURCE_DIR:="/data"}
: ${BACKUP_NAME:="data_backup"}
: ${FULL_BACKUP_DAY:="Sunday"}
: ${RETENTION_DAYS:=30}
# Main backup function
main() {
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
echo "[$timestamp] Starting backup process"
# Create directory structure
create_backup_dirs "$BACKUP_ROOT"
# Determine backup type
if should_do_full_backup; then
echo "Performing full backup"
perform_full_backup "$SOURCE_DIR" "$BACKUP_ROOT" "$BACKUP_NAME"
else
echo "Performing incremental backup"
perform_incremental_backup "$SOURCE_DIR" "$BACKUP_ROOT" "$BACKUP_NAME"
fi
# Enforce retention policy
enforce_retention_policy "$BACKUP_ROOT" "$RETENTION_DAYS" 4 12
# Verify recent backups
schedule_backup_verification "$BACKUP_ROOT"
echo "[$timestamp] Backup process completed"
}
# Error handling
trap 'echo "Backup failed at line $LINENO"; exit 1' ERR
# Run main function
main "$@"
|
🧾 See Also