Why a backup strategy matters more than the backup itself
Packing a tar archive or running a mysqldump is something anyone can do. What actually separates teams is three questions: can you restore within half an hour when something breaks, is the restored data complete, and does your backup survive if the whole server goes down? In ransomware attacks and accidental-deletion incidents hitting small sites, a surprising share of owners discover their "backup" lived only on the same disk as the production server — when the box died, the backup died with it. That is exactly why the industry treats the 3-2-1 rule as a floor, not a ceiling.
The 3-2-1 backup rule
- 3: Keep 3 copies of backups
- 2: Store on 2 different media (local disk + remote object storage)
- 1: Keep at least 1 off-site copy (ideally across zones or clouds)
The advanced variant is 3-2-1-1-0: the extra "1" is an offline or immutable copy to survive ransomware that encrypts your online backups, and the "0" means zero failed restore drills.
Choosing a backup type
| Type | How it works | Storage used | Restore speed | Best for |
|---|---|---|---|---|
| Full backup | Copies everything each run | Large | Fast | Databases, small/medium sites |
| Incremental backup | Copies only changes since last backup | Small | Slow (must replay in order) | Sites with huge files |
| Differential backup | Copies all changes since last full backup | Medium | Medium | A middle ground |
For most personal sites and small business setups, a daily full backup with 30-day retention is the most headache-free combination. Databases are usually a few MB to a few hundred MB; a full dump plus gzip costs almost nothing in complexity, so incremental logic is rarely worth it.
Automated backup script
The script below covers five stages — files, database, cleanup, off-site sync, and verification — and can run from cron every day:
#!/bin/bash
# backup.sh - automated file and database backup
set -euo pipefail
# === Config ===
SITE_DIR="/var/www/example.com"
DB_NAME="wordpress"
DB_USER="root"
DB_PASS="your_password"
BACKUP_DIR="/var/backups"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
# Remote storage config (optional)
RCLONE_REMOTE="s3:bucket-name/backups"
# === 1. Create backup directories ===
mkdir -p "$BACKUP_DIR/files" "$BACKUP_DIR/database"
# === 2. Backup files (excluding cache directories) ===
echo ">>> Backing up files..."
tar -czf "$BACKUP_DIR/files/files_$DATE.tar.gz" \
--exclude="wp-content/cache" \
--exclude="wp-content/uploads/backupwp" \
--exclude="node_modules" \
--exclude=".git" \
-C "$SITE_DIR" .
# === 3. Backup the database ===
echo ">>> Backing up database..."
mysqldump -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" \
--single-transaction --quick --lock-tables=false \
| gzip > "$BACKUP_DIR/database/db_$DATE.sql.gz"
# === 4. Delete local backups older than the retention period ===
echo ">>> Cleaning old backups..."
find "$BACKUP_DIR/files" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR/database" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
# === 5. Sync to remote storage (optional) ===
if command -v rclone &>/dev/null; then
echo ">>> Syncing to remote storage..."
rclone sync "$BACKUP_DIR" "$RCLONE_REMOTE" --progress
fi
# === 6. Verify backup integrity ===
echo ">>> Verifying backup integrity..."
tar -tzf "$BACKUP_DIR/files/files_$DATE.tar.gz" > /dev/null && echo "Files backup OK"
zcat "$BACKUP_DIR/database/db_$DATE.sql.gz" | head -1 > /dev/null && echo "Database backup OK"
echo "=== Backup complete: $DATE ==="
A few details worth noting:
--single-transactiongives a consistent snapshot under InnoDB, and--lock-tables=falseavoids locking tables for live traffic — fine on MySQL 5.6+;- Using
find -mtime +30for cleanup instead of maintaining a file list means restarts or interruptions never cause stale backups to be missed; set -euo pipefailaborts on any failure, so a silently broken backup script can't slip under the radar.
Reference: mysqldump docs https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html; rclone object storage sync https://rclone.org/s3/
Crontab entry
# Run the backup at 3:00 a.m. daily, appending logs
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
If your database is large, split the job: files at 3:00 and the database at 3:30, so disk I/O doesn't saturate and slow down live requests.
Restore steps
The payoff happens at restore time. Files:
tar -xzf /var/backups/files/files_20260713_030001.tar.gz -C /var/www/example.com/
Database:
gunzip < /var/backups/database/db_20260713_030001.sql.gz | mysql -u root -p"$PASSWORD" db_name
Verify right away: the front page loads, you can log in, and the newest row is there. A backup that has never been restored is not a backup — it is just files taking up disk space.
Consider a real case: a small team running a cross-border e-commerce site wakes up one morning to find crypto-mining code injected across the site and the database encrypted by ransomware. Because their backup script syncs the daily dump to another cloud provider's object storage, the site is back to its previous-day state within 40 minutes, losing at most a handful of order records from the last 24 hours. Meanwhile a team whose only copy lives on the same box can only stare at encrypted files and an encrypted backup. This is exactly when an off-site copy proves its worth.
Run a monthly restore drill
Put a monthly reminder on your calendar and work through three steps:
- Restore the latest backup on a clean temporary server;
- Check file permissions, database connectivity, and cache directories;
- Record how long recovery took end to end, aiming for under 30 minutes.
Suggested backup policy
| Data type | Frequency | Retention | Method |
|---|---|---|---|
| Website files | Daily | 30 days | Full + optional incremental |
| Database | Daily | 30 days | mysqldump + gzip |
| Config files | On every change | 90 days | Git repo (with history) |
| Log files | Weekly | 180 days | logrotate |
| Full snapshot | Monthly | 12 months | Provider snapshot (DR) |
Keep config and database separate: put configs in a Git repo so any change in the last 90 days can be reverted, and rely on snapshots for disaster recovery of the database.
Security notes and common mistakes
- Backups contain plaintext database data and possibly keys — set file permissions to 600 and directories to 700;
- Encrypt remote storage: use the provider's built-in encryption, or GPG-encrypt before uploading;
- Store scripts and keys separately — a leaked key is as good as a public backup;
- Three recurring mistakes: backing up files but not the database, keeping the backup on the same server, and never testing a restore. These account for most "the data is gone" stories.
16IDC Takeaway
For an individual site owner, a routine of "daily automated backup + snapshot sync to object storage + a monthly restore drill" costs almost nothing yet covers nine out of ten data-loss scenarios. What actually separates you from disaster is not how fancy your backup tooling is, but when your last successful restore test was.