Backups are the thing everyone sets up and almost nobody tests. A backup that has never been restored is not a backup — it is a hope. This guide covers automated backup strategies that survive both hardware failure and human error.
The 3-2-1 rule (still valid in 2026)
- 3 copies of your data
- 2 different media types (local disk + cloud, or local + external drive)
- 1 off-site copy (different physical location or different cloud provider)
For most small to medium WordPress sites, the practical version is: server backup + cloud object storage + one more copy somewhere else.
What to back up
A WordPress site has four components:
| Component | Location | Backup method |
|---|---|---|
| Database | MySQL/MariaDB | mysqldump or wp db export |
| Uploads | wp-content/uploads/ | rsync or tar |
| Plugins & themes | wp-content/plugins/, wp-content/themes/ | rsync or tar |
| Config | wp-config.php, .htaccess, nginx configs | rsync or tar |
Do not back up wp-content/cache/ or wp-content/upgrade/ — they are regenerated.
Database backup script
#!/bin/bash
# /usr/local/bin/db-backup.sh
set -euo pipefail
DB_NAME="wordpress"
DB_USER="root"
BACKUP_DIR="/backups/database"
RETENTION_DAYS=14
DATE=$(date +%Y-%m-%d_%H-%M)
mkdir -p "$BACKUP_DIR"
# Dump the database
mysqldump --single-transaction --quick --lock-tables=false \
-u "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_DIR/$DB_NAME-$DATE.sql.gz"
# Delete old backups
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup complete: $DB_NAME-$DATE.sql.gz"
Full site backup script
#!/bin/bash
# /usr/local/bin/site-backup.sh
set -euo pipefail
SITE_DIR="/var/www/example.com"
BACKUP_DIR="/backups/sites"
RETENTION_DAYS=7
DATE=$(date +%Y-%m-%d)
mkdir -p "$BACKUP_DIR"
# Archive the site files
tar -czf "$BACKUP_DIR/site-$DATE.tar.gz" \
--exclude="$SITE_DIR/wp-content/cache" \
--exclude="$SITE_DIR/wp-content/upgrade" \
--exclude="$SITE_DIR/wp-content/debug.log" \
"$SITE_DIR"
# Delete old backups
find "$BACKUP_DIR" -name "site-*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Site backup complete: site-$DATE.tar.gz"
Pushing to cloud storage (R2 / S3)
#!/bin/bash
# /usr/local/bin/backup-to-cloud.sh
set -euo pipefail
BACKUP_DIR="/backups"
R2_BUCKET="my-backups"
R2_ENDPOINT="https://abc123.r2.cloudflarestorage.com"
# Sync local backup dir to R2
aws s3 sync "$BACKUP_DIR" "s3://$R2_BUCKET/" \
--endpoint-url "$R2_ENDPOINT" \
--delete \
--exclude "tmp/*"
echo "Cloud sync complete"
Install and configure awscli first:
sudo apt install awscli
aws configure set aws_access_key_id YOUR_KEY
aws configure set aws_secret_access_key YOUR_SECRET
Scheduling with cron
# Database backup at 2am daily
0 2 * * * /usr/local/bin/db-backup.sh >> /var/log/backups.log 2>&1
# Full site backup at 3am daily
0 3 * * * /usr/local/bin/site-backup.sh >> /var/log/backups.log 2>&1
# Cloud sync at 4am daily
0 4 * * * /usr/local/bin/backup-to-cloud.sh >> /var/log/backups.log 2>&1
Testing your backups (the part everyone skips)
Once a month, restore to a test location:
#!/bin/bash
# Restore test script
BACKUP_FILE="/backups/database/wordpress-2026-06-01_02-00.sql.gz"
TEST_DB="wordpress_restore_test"
# Create a test database
mysql -u root -e "CREATE DATABASE IF NOT EXISTS $TEST_DB"
# Restore to test DB
gunzip < "$BACKUP_FILE" | mysql -u root "$TEST_DB"
# Verify tables exist
mysql -u root -e "SHOW TABLES FROM $TEST_DB" | wc -l
# Clean up
mysql -u root -e "DROP DATABASE $TEST_DB"
echo "Restore test passed"
If this script fails on a real backup file, your backup strategy has a hole. Fix it before you need it.
Common backup mistakes
1. Backups on the same disk
If your backup directory is on the same physical disk as your site, a disk failure kills everything. Use a separate mount point or block storage volume.
2. Only backing up the database
If you lose wp-content/uploads/, you lose years of media. Your database alone cannot rebuild your site.
3. Backups filling the disk
Without retention policies, backups accumulate until the disk is full — and then everything breaks, including the backup script.
# Check backup storage usage
du -sh /backups/*
4. Credentials in backup scripts
Do not hardcode database passwords in backup scripts. Use ~/.my.cnf:
# ~/.my.cnf
[mysqldump]
user=backupuser
password=securepassword
chmod 600 ~/.my.cnf
Then the mysqldump command does not need -u or -p flags.
WordPress-specific considerations
- Exclude transients and cache from database dumps — they bloat backups
- Use
--single-transactionfor InnoDB tables to avoid locking - Backup before every core, plugin, or theme update
- Some managed hosts include backups — verify what is included and how to access it
The backup checklist
- Database backs up daily and is verified monthly
- Files back up daily (or at least weekly)
- At least one off-site copy exists
- Retention policy prevents disk fill
- A restore test has been run in the last 30 days
- Backup scripts log output and errors
- Alerting is configured for backup failures
Backups are not a feature. They are insurance. Test them.