Skip to content

Backup Strategy

Data is the most valuable asset on a server. Hardware can be replaced, software can be reinstalled, but lost data is often unrecoverable. This article covers backup solutions from basic to advanced, helping you build a reliable data protection system.

Before defining a strategy, understand the three basic backup types:

TypeDescriptionProsCons
Full BackupBacks up all data every timeSimple and fast recoveryTime-consuming, requires more storage
Incremental BackupOnly backs up data changed since the last backupFast, saves spaceRecovery requires the full chain
Differential BackupBacks up data changed since the last full backupRelatively simple recoveryData volume grows over time

Recommended backup strategy (Grandfather-Father-Son scheme):

  • Daily: Incremental backup
  • Weekly: Full backup
  • Monthly: Full backup archived offline

rsync is the most commonly used file synchronization tool, supporting incremental transfers that only copy changed portions.

Terminal window
# Local directory sync
rsync -avh --progress /var/www/ /backup/www/
# Parameter explanation:
# -a Archive mode (preserves permissions, timestamps, symlinks, etc.)
# -v Verbose output
# -h Human-readable file sizes
# --progress Show transfer progress
Terminal window
# Push to a remote server
rsync -avhz --progress /var/www/ user@backup-server:/backup/www/
# Pull from a remote server
rsync -avhz --progress user@backup-server:/data/ /local/backup/data/
# -z enables compressed transfer, useful when bandwidth is limited
Terminal window
# Exclude directories/files that don't need to be backed up
rsync -avh \
--exclude='*.log' \
--exclude='cache/' \
--exclude='tmp/' \
--delete \
/var/www/ /backup/www/
# --delete removes extra files on the destination to keep it fully in sync
# Note: --delete is risky; for the first run, add --dry-run to preview
rsync -avhn --delete /var/www/ /backup/www/ # -n is equivalent to --dry-run
Section titled “rsync Incremental Backup Script (with Hard Links)”

Use hard links for efficient incremental backups where each backup appears to be a full copy but only occupies space for changed files:

/usr/local/bin/rsync_backup.sh
#!/bin/bash
# Incremental backup using hard links
SOURCE="/var/www"
BACKUP_BASE="/backup/www"
DATE=$(date +%Y-%m-%d_%H%M%S)
LATEST="${BACKUP_BASE}/latest"
TARGET="${BACKUP_BASE}/${DATE}"
# If a previous backup exists, use hard links
if [ -d "$LATEST" ]; then
LINK_DEST="--link-dest=${LATEST}"
else
LINK_DEST=""
fi
# Perform the backup
rsync -avh --delete $LINK_DEST "${SOURCE}/" "${TARGET}/"
# Update the latest symlink
rm -f "$LATEST"
ln -s "$TARGET" "$LATEST"
# Delete backups older than 30 days
find "$BACKUP_BASE" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
echo "[$(date)] Backup complete: ${TARGET}"

tar is suitable for creating complete packaged archives of specific directories.

Terminal window
# Create a compressed archive
tar czf /backup/www-$(date +%Y%m%d).tar.gz -C / var/www
# Use xz compression (higher compression ratio, but slower)
tar cJf /backup/www-$(date +%Y%m%d).tar.xz -C / var/www
# Exclude specific content during backup
tar czf /backup/www-$(date +%Y%m%d).tar.gz \
--exclude='*.log' \
--exclude='cache' \
-C / var/www

tar supports incremental backups based on snapshot files:

Terminal window
# First run — full backup (creates the snapshot file)
tar czf /backup/www-full-$(date +%Y%m%d).tar.gz \
--listed-incremental=/backup/www.snar \
-C / var/www
# Subsequent runs — incremental backup (based on the snapshot file)
tar czf /backup/www-incr-$(date +%Y%m%d_%H%M%S).tar.gz \
--listed-incremental=/backup/www.snar \
-C / var/www

When restoring incremental backups, you must first restore the full backup, then apply each incremental in order:

Terminal window
# Restore the full backup
tar xzf /backup/www-full-20260301.tar.gz -C /
# Restore incremental backups in order
tar xzf /backup/www-incr-20260302_020000.tar.gz -C /
tar xzf /backup/www-incr-20260303_020000.tar.gz -C /

BorgBackup (Borg for short) is a modern deduplicating and compressing backup tool, well-suited for server backup scenarios.

Terminal window
sudo dnf install epel-release -y
sudo dnf install borgbackup -y
Terminal window
# Local repository
borg init --encryption=repokey /backup/borg-repo
# Remote repository (via SSH)
borg init --encryption=repokey ssh://user@backup-server/backup/borg-repo
# You will be prompted to enter a passphrase for the encryption key — keep it safe!
Terminal window
# Create a backup archive
borg create \
--verbose \
--stats \
--progress \
--compression zstd,3 \
/backup/borg-repo::'{hostname}-{now:%Y-%m-%d_%H%M%S}' \
/var/www \
/etc \
/home \
--exclude '/home/*/.cache' \
--exclude '*.tmp'
Terminal window
# List all archives
borg list /backup/borg-repo
# View the contents of a specific archive
borg list /backup/borg-repo::myserver-2026-03-20_020000
# Restore to a specific directory
cd /tmp/restore
borg extract /backup/borg-repo::myserver-2026-03-20_020000
# Restore only a specific path
borg extract /backup/borg-repo::myserver-2026-03-20_020000 var/www
Terminal window
# Retention policy: 7 daily, 4 weekly, 6 monthly
borg prune \
--verbose \
--list \
--keep-daily=7 \
--keep-weekly=4 \
--keep-monthly=6 \
/backup/borg-repo
# Free disk space from deleted archives
borg compact /backup/borg-repo
/usr/local/bin/borg_backup.sh
#!/bin/bash
# BorgBackup automated backup script
set -euo pipefail
# Configuration
export BORG_REPO="/backup/borg-repo"
export BORG_PASSPHRASE="your_secure_passphrase" # In production, consider using a key file
BACKUP_NAME="{hostname}-{now:%Y-%m-%d_%H%M%S}"
LOG_FILE="/var/log/borg_backup.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Starting backup..."
# Create the backup
borg create \
--verbose \
--stats \
--compression zstd,3 \
--exclude '/home/*/.cache' \
--exclude '/var/tmp/*' \
--exclude '/var/log/*.gz' \
"${BORG_REPO}::${BACKUP_NAME}" \
/etc \
/home \
/var/www \
/var/lib/mysql \
2>&1 | tee -a "$LOG_FILE"
log "Backup complete, cleaning up old archives..."
# Clean up old backups
borg prune \
--verbose \
--list \
--keep-daily=7 \
--keep-weekly=4 \
--keep-monthly=6 \
"$BORG_REPO" \
2>&1 | tee -a "$LOG_FILE"
borg compact "$BORG_REPO" 2>&1 | tee -a "$LOG_FILE"
log "All operations complete."
# Unset the passphrase variable
unset BORG_PASSPHRASE
Terminal window
chmod 700 /usr/local/bin/borg_backup.sh
Terminal window
sudo crontab -e
# Run Borg backup daily at 2 AM
0 2 * * * /usr/local/bin/borg_backup.sh >> /var/log/borg_backup.log 2>&1
# Run rsync backup daily at 3 AM
0 3 * * * /usr/local/bin/rsync_backup.sh >> /var/log/rsync_backup.log 2>&1

systemd timers are more flexible than cron, with support for log integration and dependency management.

Terminal window
# Create the service unit
sudo tee /etc/systemd/system/borg-backup.service > /dev/null <<'EOF'
[Unit]
Description=BorgBackup Daily Backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/borg_backup.sh
Nice=19
IOSchedulingClass=idle
EOF
# Create the timer unit
sudo tee /etc/systemd/system/borg-backup.timer > /dev/null <<'EOF'
[Unit]
Description=Run BorgBackup daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=600
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Enable the timer
sudo systemctl daemon-reload
sudo systemctl enable --now borg-backup.timer
# Check timer status
systemctl list-timers borg-backup.timer
# Manually trigger a test run
sudo systemctl start borg-backup.service
journalctl -u borg-backup.service -f

Local backups cannot protect against datacenter-level disasters (fire, power outages, widespread hardware failure). You should maintain at least one offsite backup.

/usr/local/bin/offsite_backup.sh
#!/bin/bash
# Sync local backups to a remote server
REMOTE_USER="backup"
REMOTE_HOST="offsite-backup.example.com"
REMOTE_PATH="/backup/$(hostname)"
LOCAL_BACKUP="/backup/borg-repo"
# Use SSH key authentication (avoid password prompts)
rsync -avhz --progress \
-e "ssh -i /root/.ssh/backup_key -o StrictHostKeyChecking=yes" \
"${LOCAL_BACKUP}/" \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/"
Terminal window
# Install rclone
sudo dnf install rclone -y
# Configure (interactive)
rclone config
# Follow the prompts to configure an S3-compatible storage (e.g., AWS S3, MinIO, Alibaba Cloud OSS, etc.)
# Sync backups to object storage
rclone sync /backup/borg-repo remote:my-backup-bucket/borg-repo \
--transfers=4 \
--progress
Terminal window
# Create a temporary recovery directory
mkdir -p /tmp/restore-test
cd /tmp/restore-test
# Restore from the latest archive
export BORG_REPO="/backup/borg-repo"
export BORG_PASSPHRASE="your_secure_passphrase"
LATEST=$(borg list --last 1 --short "$BORG_REPO")
echo "Restoring archive: ${LATEST}"
borg extract "${BORG_REPO}::${LATEST}"
# Verify file integrity
echo "File count: $(find . -type f | wc -l)"
echo "Total size: $(du -sh .)"
# Compare key files
diff /etc/nginx/nginx.conf /tmp/restore-test/etc/nginx/nginx.conf
diff -r /var/www/html/ /tmp/restore-test/var/www/html/ | head -20
# Clean up
rm -rf /tmp/restore-test
unset BORG_PASSPHRASE
/usr/local/bin/test_restore.sh
#!/bin/bash
# Automated backup recovery test
set -euo pipefail
RESTORE_DIR="/tmp/restore-test-$(date +%s)"
BORG_REPO="/backup/borg-repo"
export BORG_PASSPHRASE="your_secure_passphrase"
RESULT="Success"
mkdir -p "$RESTORE_DIR"
cd "$RESTORE_DIR"
# Get the latest archive
LATEST=$(borg list --last 1 --short "$BORG_REPO")
# Attempt recovery
if borg extract "${BORG_REPO}::${LATEST}"; then
FILE_COUNT=$(find . -type f | wc -l)
TOTAL_SIZE=$(du -sh . | awk '{print $1}')
# Basic validation
if [ "$FILE_COUNT" -eq 0 ]; then
RESULT="Failed - restored file count is 0"
fi
else
RESULT="Failed - borg extract returned an error"
fi
# Send report
REPORT="Backup Recovery Test Report\n\n"
REPORT+="Date: $(date)\n"
REPORT+="Archive: ${LATEST}\n"
REPORT+="Result: ${RESULT}\n"
REPORT+="File Count: ${FILE_COUNT:-N/A}\n"
REPORT+="Data Size: ${TOTAL_SIZE:-N/A}\n"
echo -e "$REPORT" | mail -s "[Backup Test] Recovery Test ${RESULT}" "$MAILTO"
# Clean up
rm -rf "$RESTORE_DIR"
unset BORG_PASSPHRASE

Add the recovery test to a scheduled task; running it once a month is recommended:

Terminal window
# Run recovery test at 5 AM on the 1st of every month
0 5 1 * * /usr/local/bin/test_restore.sh >> /var/log/restore_test.log 2>&1

In addition to file backups, databases require their own backup strategies.

Terminal window
# Full backup
mysqldump --all-databases --single-transaction --quick \
--routines --triggers \
-u root -p | gzip > /backup/mysql-$(date +%Y%m%d).sql.gz
# Specific database
mysqldump --single-transaction -u root -p mydb | gzip > /backup/mydb-$(date +%Y%m%d).sql.gz
# Restore
gunzip < /backup/mysql-20260320.sql.gz | mysql -u root -p
Terminal window
# Full backup
sudo -u postgres pg_dumpall | gzip > /backup/postgres-$(date +%Y%m%d).sql.gz
# Specific database
sudo -u postgres pg_dump mydb | gzip > /backup/mydb-$(date +%Y%m%d).sql.gz
# Restore
gunzip < /backup/postgres-20260320.sql.gz | sudo -u postgres psql

When planning a backup solution, verify each of the following:

  • Clearly define the scope of data to back up (configuration files, website data, databases, user data)
  • Choose appropriate backup tools and types
  • Set backup frequency (daily/weekly/monthly)
  • Configure automated scheduling
  • Set a backup retention policy (how many copies, how long to keep)
  • Maintain at least one offsite backup
  • Encrypt backup data (especially for offsite/cloud backups)
  • Perform a recovery test at least once a month
  • Monitor whether backup tasks complete successfully on schedule
  • Document the complete recovery procedure