Disaster Recovery
Disaster recovery (DR) is about getting back online fast. Whether your server crashed, got hacked, or a bad deployment broke everything — rsync-based backups let you restore your services in minutes, not hours.
Recovery Scenarios
| Scenario | What Failed | Recovery Method |
|---|---|---|
| Server crash | Hardware/OS failure | Restore files + DB to new server |
| Bad deployment | Code broke the site | Restore from last known-good backup |
| Security breach | Site hacked/malware | Restore from pre-compromise backup |
| Accidental deletion | Files/DB deleted by mistake | Restore specific files from snapshot |
| Data center failure | Entire facility down | Failover to standby or rebuild from offsite |
Restoring from Backups
Restore Full Site (Files + Database)
# Step 1: Restore files from backup
rsync -av /backup/2024-01-15/files/ /var/www/html/
# Step 2: Restore database
gunzip < /backup/2024-01-15/databases.sql.gz | mysql -u root -p
# Step 3: Fix permissions
sudo chown -R www-data:www-data /var/www/html
# Step 4: Restart web server
sudo systemctl restart nginx # or apache2, openlitespeed, etc.
Restore from Incremental Snapshot
With --link-dest backups, every snapshot is a complete copy:
# List available snapshots
ls -la /backup/
# 2024-01-13/
# 2024-01-14/
# 2024-01-15/
# latest -> 2024-01-15
# Restore from any date
rsync -av /backup/2024-01-14/files/ /var/www/html/
Restore Specific Files Only
# Restore a single config file
cp /backup/latest/files/wp-config.php /var/www/html/
# Restore an entire directory
rsync -av /backup/latest/files/wp-content/plugins/ /var/www/html/wp-content/plugins/
# Restore from a specific date
rsync -av /backup/2024-01-10/files/wp-content/uploads/2024/ \
/var/www/html/wp-content/uploads/2024/
Restoring to a New Server
When the original server is unrecoverable:
# On the new server:
# Step 1: Install prerequisites
sudo apt update && sudo apt install -y nginx mariadb-server php-fpm rsync
# Step 2: Pull backup from remote storage
rsync -avzP user@backup-server:/backups/latest/ /restore/
# Step 3: Restore files
rsync -av /restore/files/ /var/www/html/
# Step 4: Restore database
gunzip < /restore/databases.sql.gz | mysql -u root -p
# Step 5: Update DNS to point to new server IP
echo "Update DNS A record to: $(curl -s ifconfig.me)"
Hot Standby Server
Keep a secondary server permanently synced and ready to take over:
# Hourly sync to standby (via cron)
0 * * * * rsync -avz --delete \
/var/www/html/ standby@dr-server:/var/www/html/
# Hourly database sync
0 * * * * mysqldump --defaults-file=/root/.my.cnf \
--single-transaction --all-databases | \
ssh standby@dr-server "mysql"
Failover Process
flowchart LR
P["Primary Server<br/>(fails)"] -. "rsync hourly" .-> S["Standby Server<br/>(takes over)"]
DNS["DNS Record"] --> P
DNS -. "update during failover" .-> S
- Detect primary server failure
- Verify standby is current
- Update DNS to point to standby IP
- Monitor until primary is rebuilt
Automated Recovery Script
#!/bin/bash
# disaster-recovery.sh — Restore a server from backup
set -euo pipefail
BACKUP_SOURCE="user@backup-server:/backups/latest"
WEB_ROOT="/var/www/html"
LOG="/var/log/disaster-recovery.log"
echo "=== DR started: $(date) ===" | tee -a "$LOG"
# Restore files
echo "Restoring files..." | tee -a "$LOG"
rsync -avz --stats "$BACKUP_SOURCE/files/" "$WEB_ROOT/" >> "$LOG" 2>&1
# Restore database
echo "Restoring database..." | tee -a "$LOG"
ssh user@backup-server "cat /backups/latest/databases.sql.gz" | \
gunzip | mysql -u root >> "$LOG" 2>&1
# Fix permissions
echo "Fixing permissions..." | tee -a "$LOG"
sudo chown -R www-data:www-data "$WEB_ROOT"
sudo find "$WEB_ROOT" -type d -exec chmod 755 {} \;
sudo find "$WEB_ROOT" -type f -exec chmod 644 {} \;
# Restart services
sudo systemctl restart nginx php*-fpm mariadb
echo "=== DR complete: $(date) ===" | tee -a "$LOG"
echo " Site should be live. Verify at: https://your-domain.com"
DR Drills (Testing Your Recovery)
warning
A recovery plan you've never tested is not a plan — it's a wish.
Monthly Restore Test
# Restore to a staging server
rsync -av /backup/latest/files/ /var/www/staging/
gunzip < /backup/latest/databases.sql.gz | mysql staging_db
# Verify the restored site works
curl -s -o /dev/null -w "%{http_code}" http://staging.example.com
# Should return 200
DR Checklist
- Can you locate your most recent backup?
- Do you know which server/path holds offsite copies?
- Can you restore both files and database?
- Do you have SSH access to backup storage?
- Have you tested a full restore in the last 30 days?
- Is your recovery time acceptable for your SLA?
Common Pitfalls
| Pitfall | Consequence | Prevention |
|---|---|---|
| Restoring without fixing permissions | Site errors (403, 500) | chown and chmod after restore |
| Restoring files but forgetting database | Site looks broken, missing content | Always restore both |
| Restoring a compromised backup | Malware comes back | Identify compromise date, restore from before it |
| No offsite backup | Server room fire = total loss | Always keep 1+ offsite copy |
| Never testing recovery | Discover backup is incomplete during crisis | Monthly DR drills |
| DNS propagation delay | Users still hitting old server | Use low TTL on DNS records |
Quick Reference
# Restore files from latest backup
rsync -av /backup/latest/files/ /var/www/html/
# Restore database
gunzip < /backup/latest/databases.sql.gz | mysql -u root -p
# Restore from specific date
rsync -av /backup/2024-01-14/files/ /var/www/html/
# Restore from remote backup
rsync -avzP user@backup:/backups/latest/files/ /var/www/html/
# Fix permissions after restore
sudo chown -R www-data:www-data /var/www/html
What's Next
- Backup Strategies — Build the backup system that feeds DR
- Cloud Storage Integration — Offsite backups for worst-case scenarios
- Secure Transfer — Protect backup data in transit