💾 Backup Strategies

Backup & Recovery Series: Part 1 — Backup Strategies  |  Part 2 — rsync Deep Dive  |  Part 3 — Recovery Procedures

A Backup You Haven't Tested Is Not a Backup

Backups are not optional — they are the last line of defense against hardware failure, ransomware, accidental deletion, and human error. The question is not whether to back up but how: what to back up, how often, where to store it, and critically — how to verify you can actually restore from it.

This page covers backup strategy fundamentals: full vs incremental vs differential, the 3-2-1 rule, tar and rsync patterns, and what to include in every backup plan. Part 2 covers rsync in depth. Part 3 covers recovery procedures when things go wrong.

Backup Strategy Fundamentals

TypeWhat it backs upSpeedRestoreBest for
FullEverything every timeSlowSingle file neededWeekly anchor backup
IncrementalChanges since last backup (any type)FastFull + all incrementalsDaily backups
DifferentialChanges since last FULL backupMediumFull + latest differentialBalance of speed and simplicity
The 3-2-1 Rule — minimum viable backup strategy: One copy on the server, one copy on a backup server on the same network, one copy offsite or in cloud storage. Ransomware that encrypts your server and your backup server cannot reach the offsite copy.

Examples

1
tar — Archive and Compress
# Create a compressed archive
tar -czf backup-$(date +%Y%m%d).tar.gz /var/www/html/
tar -czf /backup/etc-$(date +%Y%m%d).tar.gz /etc/

# Create with bzip2 compression (smaller, slower)
tar -cjf backup-$(date +%Y%m%d).tar.bz2 /var/www/html/

# Create with xz compression (smallest, slowest)
tar -cJf backup-$(date +%Y%m%d).tar.xz /var/www/html/

# List contents without extracting
tar -tzf backup-20260425.tar.gz
tar -tzf backup-20260425.tar.gz | grep "nginx.conf"

# Extract entire archive
tar -xzf backup-20260425.tar.gz

# Extract to a specific directory
tar -xzf backup-20260425.tar.gz -C /restore/

# Extract a single file
tar -xzf backup-20260425.tar.gz var/www/html/index.html

# Show progress during creation
tar -czf backup.tar.gz /var/www/html/ | pv -l > /dev/null
💡 Always exclude what you don't need.
tar -czf backup.tar.gz \
    --exclude='/var/www/html/cache' \
    --exclude='*.tmp' \
    --exclude='*.log' \
    /var/www/html/
Excluding cache directories, temp files, and logs keeps archives small and fast without losing anything important.
2
rsync — Fast Incremental Backup
# Basic rsync backup (local)
rsync -av /var/www/html/ /backup/www/

# rsync with delete (mirror -- removes files deleted from source)
rsync -av --delete /var/www/html/ /backup/www/

# rsync to a remote server over SSH
rsync -avz /var/www/html/ user@backupserver:/backup/www/

# rsync from remote to local (pull backup)
rsync -avz user@webserver:/var/www/html/ /backup/www/

# Dry run -- show what would be transferred without doing it
rsync -avnz --delete /var/www/html/ /backup/www/

# Key flags:
# -a  archive mode (recursive, preserves permissions, timestamps, symlinks)
# -v  verbose
# -z  compress during transfer (good for slow links)
# -n  dry run
# --delete  remove files from destination not in source
# --progress  show per-file progress
# --stats  show summary statistics
rsync only transfers what changed. The first rsync run transfers everything. Subsequent runs only transfer files that have been added or modified. A 10GB web directory that changes 100MB per day takes hours the first time and minutes every day after. This is why rsync is the backbone of most Linux backup strategies.
3
What to Back Up — The Essential List
#!/bin/bash
# backup-essentials.sh -- back up everything that matters

DEST=/backup
DATE=$(date +%Y%m%d)
LOG=/var/log/backup.log

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a $LOG; }

log "=== Backup started ==="

# /etc -- ALL system configuration
log "Backing up /etc"
tar -czf $DEST/etc-$DATE.tar.gz /etc/ 2>>/var/log/backup-errors.log

# Home directories
log "Backing up /home"
tar -czf $DEST/home-$DATE.tar.gz /home/ 2>>/var/log/backup-errors.log

# Web content
log "Backing up web content"
rsync -az --delete /var/www/ $DEST/www/ 2>>/var/log/backup-errors.log

# Databases -- dump before backing up files
log "Dumping databases"
mysqldump --all-databases | gzip > $DEST/mysql-$DATE.sql.gz 2>>/var/log/backup-errors.log
pg_dumpall | gzip > $DEST/postgres-$DATE.sql.gz 2>>/dev/null

# Custom application data
log "Backing up application data"
rsync -az /opt/myapp/data/ $DEST/appdata/ 2>>/var/log/backup-errors.log

# Crontabs
crontab -l > $DEST/crontab-root-$DATE.txt 2>/dev/null
for user in $(cut -f1 -d: /etc/passwd); do
    crontab -l -u $user > $DEST/crontab-$user-$DATE.txt 2>/dev/null
done

log "=== Backup completed ==="
Always dump databases before backing up their files. Backing up the raw MySQL or PostgreSQL data directory while the database is running produces an inconsistent backup that may not restore correctly. Always use mysqldump or pg_dump to get a consistent snapshot, then back up the dump file.
4
Snapshot Backups with Hard Links

Hard-link snapshots give you multiple point-in-time backups that look like full backups but use the space of incrementals. This is the technique behind Time Machine and many professional backup systems:

# The --link-dest trick -- files unchanged since yesterday are hard-linked
# instead of copied. Space used = only the changed files.

BACKUP_ROOT=/backup/snapshots
TODAY=$(date +%Y%m%d)
YESTERDAY=$(date -d yesterday +%Y%m%d)

rsync -av --delete \
    --link-dest=$BACKUP_ROOT/$YESTERDAY \
    /var/www/html/ \
    $BACKUP_ROOT/$TODAY/

# Result: each day's directory looks like a full backup
# but unchanged files are just hard links -- no extra disk space
ls $BACKUP_ROOT/
# 20260420/  20260421/  20260422/  20260423/  20260424/  20260425/
# Each directory appears to contain all files
# But total disk usage is only slightly more than one full backup

# Clean up snapshots older than 30 days
find $BACKUP_ROOT -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;
💡 --link-dest is rsync's superpower for backups. 30 daily snapshots that each look like full backups, using only slightly more disk space than a single full backup. You can browse any day's snapshot like a regular directory and restore individual files instantly. See Part 2 for the full rsync deep dive.
5
Verify Your Backups — The Critical Step
# Verify tar archive integrity
tar -tzf backup-20260425.tar.gz > /dev/null
echo "Archive OK: $?"

# Verify and list files
tar -tvzf backup-20260425.tar.gz | wc -l
echo "Files in archive: $(tar -tzf backup-20260425.tar.gz | wc -l)"

# Test extraction to a temp directory
mkdir -p /tmp/restore-test
tar -xzf backup-20260425.tar.gz -C /tmp/restore-test
ls -la /tmp/restore-test/
rm -rf /tmp/restore-test

# Verify rsync backup matches source
rsync -avnc /var/www/html/ /backup/www/ 2>&1 | grep -v "^sending\|^sent\|^total"
# -c = checksum verification (slower but thorough)

# Check backup file sizes are reasonable
ls -lh /backup/*.tar.gz
du -sh /backup/www/

# Monthly restore test -- actually restore something
#!/bin/bash
# Pick a random file, restore it, verify it matches original
TEST_FILE=$(tar -tzf /backup/etc-$(date +%Y%m%d).tar.gz | shuf -n 1)
tar -xzf /backup/etc-$(date +%Y%m%d).tar.gz -C /tmp/restore-test "$TEST_FILE"
diff /$TEST_FILE /tmp/restore-test/$TEST_FILE && echo "RESTORE VERIFIED" || echo "RESTORE FAILED"
⚠️ A backup you have never tested is not a backup. The worst time to discover your backup is corrupt or incomplete is when you need to restore from it. Test restores monthly. Pick random files, restore them, verify they match. Run a full test restore to a staging environment at least annually. Document the restore procedure so someone else can do it under pressure.
6
Offsite and Cloud Backup
# rclone -- sync to cloud storage (S3, GCS, Backblaze, etc.)
sudo dnf install -y rclone    # or download from rclone.org

# Configure rclone (interactive)
rclone config

# Sync backup directory to S3
rclone sync /backup/ s3:mybucket/server-backups/

# Copy with progress
rclone copy --progress /backup/etc-$(date +%Y%m%d).tar.gz s3:mybucket/

# Encrypt before uploading (always encrypt offsite backups)
gpg --symmetric --cipher-algo AES256 backup-20260425.tar.gz
rclone copy backup-20260425.tar.gz.gpg s3:mybucket/encrypted/

# rsync to an offsite server
rsync -avz --delete /backup/ offsite-user@offsite-server:/backup/$(hostname)/

# sftp/scp for simple file transfers
scp /backup/etc-$(date +%Y%m%d).tar.gz user@offsite:/backup/
Always encrypt offsite backups. Backups contain sensitive data — configuration files, credentials, user data. Before sending anything offsite or to cloud storage, encrypt it with GPG or use a backup tool with built-in encryption. The backup destination is outside your security perimeter.
7
Backup Retention Policy
#!/bin/bash
# backup-retention.sh -- implement grandfather-father-son retention

BACKUP_DIR=/backup
DATE=$(date +%Y%m%d)
DOW=$(date +%u)    # 1=Monday, 7=Sunday
DOM=$(date +%d)    # day of month

# Son: daily backups, keep 7 days
tar -czf $BACKUP_DIR/daily/backup-$DATE.tar.gz /var/www/html/
find $BACKUP_DIR/daily/ -name "backup-*.tar.gz" -mtime +7 -delete

# Father: weekly backup on Sunday, keep 4 weeks
if [[ "$DOW" == "7" ]]; then
    cp $BACKUP_DIR/daily/backup-$DATE.tar.gz $BACKUP_DIR/weekly/
    find $BACKUP_DIR/weekly/ -name "backup-*.tar.gz" -mtime +28 -delete
fi

# Grandfather: monthly backup on 1st, keep 12 months
if [[ "$DOM" == "01" ]]; then
    cp $BACKUP_DIR/daily/backup-$DATE.tar.gz $BACKUP_DIR/monthly/
    find $BACKUP_DIR/monthly/ -name "backup-*.tar.gz" -mtime +365 -delete
fi

echo "Backup complete: $(du -sh $BACKUP_DIR)"
Grandfather-Father-Son retention: Daily backups for a week, weekly backups for a month, monthly backups for a year. This gives you fine-grained recovery for recent events (last 7 days) and long-term recovery points (monthly for a year) without keeping every daily backup forever. Adjust the retention periods for your compliance requirements.
8
Backup Health Check Script
#!/bin/bash
# backup-health.sh -- verify backups ran and are fresh

BACKUP_DIR=/backup
MAX_AGE_HOURS=25    # alert if backup is older than this
MIN_SIZE_MB=10      # alert if backup is smaller than this (suspiciously small)
ALERT_EMAIL=admin@example.com

alert() { echo "$*" | mail -s "BACKUP ALERT: $(hostname)" $ALERT_EMAIL; echo "ALERT: $*"; }

echo "=== Backup Health Check: $(hostname) ==="
echo "=== $(date) ==="

# Check each expected backup file
for pattern in "daily/backup-*.tar.gz" "www/" "mysql-*.sql.gz"; do
    LATEST=$(ls -t $BACKUP_DIR/$pattern 2>/dev/null | head -1)

    if [[ -z "$LATEST" ]]; then
        alert "Missing backup: $pattern not found"
        continue
    fi

    # Check age
    AGE_HOURS=$(( ( $(date +%s) - $(stat -c %Y "$LATEST") ) / 3600 ))
    if [[ $AGE_HOURS -gt $MAX_AGE_HOURS ]]; then
        alert "Stale backup: $LATEST is ${AGE_HOURS}h old (max ${MAX_AGE_HOURS}h)"
    else
        echo "OK ($AGE_HOURS h old): $LATEST"
    fi

    # Check size
    SIZE_MB=$(du -sm "$LATEST" | awk '{print $1}')
    if [[ $SIZE_MB -lt $MIN_SIZE_MB ]]; then
        alert "Suspiciously small backup: $LATEST is only ${SIZE_MB}MB"
    fi
done

echo "Backup dir usage: $(du -sh $BACKUP_DIR)"

Quick Reference

CommandWhat it does
tar -czf backup.tar.gz /path/Create compressed archive
tar -tzf backup.tar.gzList archive contents
tar -xzf backup.tar.gz -C /restore/Extract to directory
rsync -av --delete src/ dest/Mirror with deletion
rsync -avz src/ user@host:/dest/Remote backup over SSH
rsync -avnz src/ dest/Dry run
rsync --link-dest=PREV src/ TODAY/Hard-link snapshot backup
mysqldump --all-databases | gzip > db.sql.gzDump all MySQL databases
pg_dumpall | gzip > pg.sql.gzDump all PostgreSQL databases
gpg --symmetric file.tar.gzEncrypt backup file
rclone sync /backup/ s3:bucket/Sync to cloud storage