Linux: Phase 4 Mini Project — Automated Backup Script
The Phase 4 capstone project. Combine process management, package management, archiving/compression, and all Bash scripting skills to build a usable automated backup tool.
📋 Prerequisites: You should already know
- Lesson 20: Bash Scripting Basics
- Lesson 21: Bash Scripting Advanced
1. What You'll Learn
- Script architecture design (function encapsulation)
- Incremental backup strategy
- Remote rsync sync
- Logging and error handling
- cron scheduled execution
2. Xiaoming's Server Hard Drive Failed
(1) The Pain Point: Hard Drive Failed, Data Lost
Xiaoming's server hard drive suddenly failed, losing 3 days of data. He realized he had no backups — and manual backup was too tedious, he always forgot.
(2) Automated Backup Solution
He used Phase 4 skills to write a complete backup tool:
# Backup locally
./backup.sh -s /var/www -d /backup
# Backup remotely
./backup.sh -s /var/www -d user@backup:/backups
# Add to cron for daily execution
0 2 * * * /opt/scripts/backup.sh -s /var/www -d /backup
(3) The Benefit: No More Fear of Data Loss
The backup script runs automatically every day at 2 AM, incrementally syncing to a remote location. Xiaoming can finally sleep peacefully.
3. Knowledge Points
(1) Backup Script Architecture Design
A good backup script should be modular:
backup.sh
├── Config loading
├── Checks (directory/dependencies/space)
├── Compression (source → tar.gz)
├── Sync (local or remote rsync)
├── Cleanup (delete expired backups)
├── Logging
└── Notification (success/failure)
(2) Incremental Backup Strategy
# Method 1: rsync --link-dest (recommended)
# Uses hard links; each day appears as a full directory but only stores changes
rsync -av --link-dest=/backup/yesterday/ /data/ /backup/today/
> 💡 Tip: The incremental backup principle of `rsync --link-dest` leverages hard links — unchanged files in "today's" backup are just hard links pointing to "yesterday's" files, consuming no extra disk space; only changed files are actually written as new data. Outwardly, each day looks like a full directory, but disk usage is close to incremental.
# Method 2: rsync incremental transfer
# rsync only transfers changed parts by default
rsync -avz --delete /data/ user@host:/backup/
> ⚠️ Warning: `rsync --delete` makes the destination match the source exactly — files in the destination that don't exist in the source will be deleted. If you get the source path wrong (e.g., missing the trailing `/`), the destination could be emptied. Always use `--dry-run` first to preview.
# Method 3: Date-named archives
backup-2026-07-07.tar.gz
backup-2026-07-08.tar.gz # Two independent files, but more space
ℹ️ Note: Professional backup strategies follow the "3-2-1 rule" — 3 copies of data, 2 different storage media, 1 off-site backup. Local backup + remote rsync forms the basis of a 3-2-1 strategy. Relying solely on local tar.gz isn't enough — a hard drive failure would destroy both source data and backup.
(3) cron Scheduling
# cron format: minute hour day month weekday command
# Run backup daily at 2:00 AM
0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1
# Run every hour
0 * * * * /opt/scripts/backup-hourly.sh
# Every Monday at 3:00 AM
0 3 * * 1 /opt/scripts/backup-weekly.sh
(4) ▶ Example: Check Backup Space
#!/bin/bash
# Check if backup directory has enough space
check_space() {
local dest="$1"
local min_gb="${2:-1}"
local available=$(df -BG "$dest" | tail -1 | awk '{print $4}' | tr -d 'G')
if [ "$available" -lt "$min_gb" ]; then
echo "Error: Insufficient disk space (available: ${available}G, needed: ${min_gb}G)"
return 1
fi
echo "Disk space sufficient: ${available}G"
return 0
}
Output:
TEXTDisk space sufficient: 50G
(5) ▶ Example: Logging Function
#!/bin/bash
LOG_FILE="/var/log/backup.log"
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
log "INFO" "Backup started"
log "ERROR" "Backup failed: insufficient disk space"
log "INFO" "Backup complete"
(6) ▶ Example: Lock Mechanism to Prevent Duplicate Execution
#!/bin/bash
LOCK_FILE="/tmp/backup.lock"
# Try to acquire lock
exec 200>"$LOCK_FILE"
flock -n 200 || {
echo "Backup script is already running (lock file: $LOCK_FILE)"
exit 1
}
# ... backup logic ...
# Lock is automatically released when script exits
(7) ▶ Example: Clean Up Expired Backups
#!/bin/bash
cleanup_old_backups() {
local backup_dir="$1"
local retention_days="${2:-30}"
log "INFO" "Cleaning backups older than $retention_days days..."
local count=0
while IFS= read -r -d '' file; do
rm -f "$file"
count=$((count + 1))
done < <(find "$backup_dir" -name "*.tar.gz" -type f -mtime +$retention_days -print0)
if [ $count -gt 0 ]; then
log "INFO" "Cleaned up $count expired files"
fi
}
(8) ▶ Comprehensive Example: Complete Backup Tool Script
#!/bin/bash
# backup.sh - Automated backup tool
# Usage: ./backup.sh -s <source_dir> -d <dest_dir> [-r <retention_days>] [-e <exclude_pattern>] [-v]
set -euo pipefail
# ====== Configuration ======
VERSION="1.0.0"
LOG_FILE="/var/log/backup.log"
LOCK_FILE="/tmp/backup.lock"
DEFAULT_RETENTION=30
# ====== Colors ======
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# ====== Functions ======
usage() {
cat << EOF
Backup Tool v${VERSION}
Usage: $0 -s <source_dir> -d <dest_dir> [options]
Required:
-s <source_dir> Source directory to back up
-d <dest_dir> Backup destination (local path or user@host:/path)
Options:
-r <days> Retention days (default: $DEFAULT_RETENTION)
-e <pattern> Exclude pattern (can be used multiple times, e.g., -e node_modules -e .git)
-v Verbose output
-t Test mode (no actual backup created)
-h Show help
EOF
exit 0
}
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
info() { log "INFO" "$1"; }
success() { log "INFO" "${GREEN}$1${NC}"; }
warn() { log "WARN" "${YELLOW}$1${NC}"; }
error() { log "ERROR" "${RED}$1${NC}"; }
check_deps() {
for cmd in rsync tar; do
if ! command -v "$cmd" &> /dev/null; then
error "Required command '$cmd' not found"
exit 1
fi
done
}
check_disk_space() {
local dir="$1"
local min_gb="${2:-1}"
if [[ "$dir" != *:* ]]; then
local avail=$(df -BG "$dir" 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G')
if [ "$avail" -lt "$min_gb" ]; then
error "Insufficient disk space: ${avail}G available, ${min_gb}G needed"
return 1
fi
info "Disk space: ${avail}G available"
fi
}
cleanup_old() {
local dir="$1"
local days="$2"
if [[ "$dir" != *:* ]]; then
local count=$(find "$dir" -name "*.tar.gz" -type f -mtime +$days -delete -print | wc -l)
if [ $count -gt 0 ]; then
info "Cleaned up $count expired backups"
fi
fi
}
# ====== Lock mechanism ======
exec 200>"$LOCK_FILE"
flock -n 200 || {
error "Backup script is already running (PID: $(cat $LOCK_FILE 2>/dev/null))"
exit 1
}
echo $$ > "$LOCK_FILE"
# ====== Signal handling ======
cleanup() {
rm -f "$LOCK_FILE"
info "Backup script exiting"
}
trap cleanup EXIT INT TERM
# ====== Parameter parsing ======
SOURCE=""
DEST=""
RETENTION=$DEFAULT_RETENTION
EXCLUDE=()
VERBOSE=false
DRY_RUN=false
while getopts "s:d:r:e:vth" opt; do
case $opt in
s) SOURCE="$OPTARG" ;;
d) DEST="$OPTARG" ;;
r) RETENTION="$OPTARG" ;;
e) EXCLUDE+=("$OPTARG") ;;
v) VERBOSE=true ;;
t) DRY_RUN=true ;;
h) usage ;;
*) usage ;;
esac
done
# ====== Validation ======
if [ -z "$SOURCE" ] || [ -z "$DEST" ]; then
error "Must specify -s (source directory) and -d (destination)"
usage
fi
if [ ! -d "$SOURCE" ]; then
error "Source directory '$SOURCE' does not exist"
exit 1
fi
# ====== Main logic ======
info "========================================"
info "Backup Tool v${VERSION}"
info "Source: $SOURCE"
info "Destination: $DEST"
info "Retention: $RETENTION days"
if [ "$DRY_RUN" = true ]; then
warn "Test mode - no actual backup will be created"
fi
info "========================================"
# 1. Check dependencies and space
check_deps
check_disk_space "$DEST"
# 2. Generate filename
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$(basename "$SOURCE")_${TIMESTAMP}.tar.gz"
# 3. Build exclude arguments
EXCLUDE_ARGS=""
for pattern in "${EXCLUDE[@]}"; do
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$pattern"
done
# 4. Create compressed backup
if [ "$DRY_RUN" = false ]; then
info "Creating compressed backup: $BACKUP_NAME"
# Local backup
if [[ "$DEST" != *:* ]]; then
mkdir -p "$DEST"
eval tar -czf "${DEST}/${BACKUP_NAME}" $EXCLUDE_ARGS "$SOURCE"
success "✅ Local backup complete: ${DEST}/${BACKUP_NAME}"
# Sync to remote
if [ "$VERBOSE" = true ]; then
ls -lh "${DEST}/${BACKUP_NAME}"
fi
else
# Remote backup - compress locally then rsync
local_temp="/tmp/${BACKUP_NAME}"
eval tar -czf "$local_temp" $EXCLUDE_ARGS "$SOURCE"
info "Transferring to remote: $DEST"
rsync -avz --progress "$local_temp" "${DEST}/${BACKUP_NAME}"
rm -f "$local_temp"
success "✅ Remote backup complete: ${DEST}/${BACKUP_NAME}"
fi
else
warn "Test mode: Skipping backup creation"
fi
# 5. Clean up expired backups
if [ "$DRY_RUN" = false ] && [ $RETENTION -gt 0 ]; then
cleanup_old "$DEST" $RETENTION
fi
# 6. Backup summary
info "Backup summary:"
info " Source: $SOURCE"
info " Destination: $DEST"
if [ "$DRY_RUN" = false ]; then
info " File: ${BACKUP_NAME}"
if [[ "$DEST" != *:* ]]; then
info " Size: $(du -h "${DEST}/${BACKUP_NAME}" | cut -f1)"
fi
fi
info "========================================"
success "✅ Backup operation complete"
# Usage examples
# 1. Basic usage
./backup.sh -s /var/www/myapp -d /backup
# 2. Exclude directories + retain 7 days
./backup.sh -s /var/www/myapp -d /backup -e node_modules -e .git -r 7
# 3. Remote backup
./backup.sh -s /var/www -d user@backup-server:/backups -v
# 4. Test mode (no actual backup)
./backup.sh -s /var/www -d /backup -t
# 5. Add to cron (daily at 2 AM)
crontab -e
# Add:
0 2 * * * /opt/scripts/backup.sh -s /var/www/myapp -d /backup -e node_modules -r 30 > /dev/null 2>&1
❓ FAQ
Q: How to prevent the backup script from running twice simultaneously? A: Use file locking (flock): when the script starts, it tries to create a lock file. If the lock already exists, it exits. This way, even if cron triggers again before the backup finishes, two backups won't run concurrently.
Q: How long should backup files be retained? A: It depends on storage space and compliance requirements. Common strategy: daily backups for 30 days, weekly backups for 3 months, monthly backups for 1 year. Use
--link-destor different cron frequencies to implement.
Q: How to verify backup integrity? A: Use
sha256sumto generate a checksum file:sha256sum backup.tar.gz > backup.sha256. When restoring, verify withsha256sum -c backup.sha256.
Q: Will rsync resume if a transfer is interrupted? A: Yes. rsync transfers incrementally by default. Even if an in-progress file transfer is interrupted, the next rsync run will only transfer the unfinished portion. Add
--partialto keep partially transferred files.
Q: What needs to be configured for remote server backup? A: 1) SSH key authentication (
ssh-keygen+ssh-copy-id). 2) Remote target directory exists. 3) Remote user has write permissions. 4) Optional: configure SSH passwordless login.
📖 Summary
- Backup script architecture: check → compress → sync → cleanup → log
flockprevents duplicate executionrsync --deletesyncs and removes extra files on destination- Retention policy:
find ... -mtime +N -deletecleans expired backups - cron scheduling:
0 2 * * * /path/to/backup.sh
📝 Exercises
- Basic (⭐): Create a test directory and files, use
backup.shto back up to/tmp/test-backup/, then add-eto exclude*.logfiles and verify the exclusion worked - Intermediate (⭐⭐): Configure cron to run a backup every hour (observe 3+ runs), and change the backup to remote rsync (if you have multiple machines available)
- Challenge (⭐⭐⭐): Add email or system notifications (
wall,mail, or webhook) to the script, implement backup integrity verification (sha256sum), and auto-alert on backup failure