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:

ComponentLocationBackup method
DatabaseMySQL/MariaDBmysqldump or wp db export
Uploadswp-content/uploads/rsync or tar
Plugins & themeswp-content/plugins/, wp-content/themes/rsync or tar
Configwp-config.php, .htaccess, nginx configsrsync 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-transaction for 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.