Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 064 / 100

Backups for WordPress: a practical guide for developers

Recovery you can trust.

Expona Admin8 min read

Introduction

We've all heard the horror stories: a plugin update goes wrong, a security breach wipes out data, or a hosting provider suffers hardware failure—and suddenly your WordPress site is gone. Backups for WordPress: a practical guide for developers isn't just about having copies of your files and database sitting somewhere; it's about having a tested, reliable recovery strategy you can execute under pressure. After years of managing production WordPress sites across e-commerce, SaaS, and publishing platforms, we know that backups are only useful if you've actually tested restoring from them.

This guide walks you through a backup strategy that works in real environments: automated daily captures, off-site storage, restoration testing, and pre-update snapshots. You'll learn not just the "how" but the "why" behind each practice, so you can adapt it to your specific infrastructure and risk tolerance.

Daily database and file backups

A solid backup routine captures two distinct things: your WordPress database (posts, users, settings, plugin data) and your files (plugins, themes, uploads). They're equally critical, and they need different handling.

Database backups should run daily, ideally during off-peak hours. WordPress databases can grow quickly, especially on high-traffic sites or those with extensive media libraries. A daily backup means you lose at most 24 hours of data—a reasonable recovery point objective (RPO) for most sites.

Here's a practical approach using mysqldump with a cron job:

php
#!/bin/bash
# Daily database backup script

BACKUP_DIR="/backups/wordpress"
DB_NAME="wordpress_db"
DB_USER="wp_user"
DB_PASSWORD="secure_password"
DB_HOST="localhost"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/db_backup_${TIMESTAMP}.sql"

# Create backup
mysqldump -h ${DB_HOST} -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME} > ${BACKUP_FILE}

# Compress to save space
gzip ${BACKUP_FILE}

# Remove backups older than 30 days
find ${BACKUP_DIR} -name "db_backup_*.sql.gz" -mtime +30 -delete

echo "Database backup completed: ${BACKUP_FILE}.gz"

Schedule this in crontab to run at 2 AM daily:

php
0 2 * * * /home/scripts/backup_database.sh >> /var/log/backup.log 2>&1

File backups capture your /wp-content directory (plugins, themes, uploads) and critical config files like wp-config.php. Full daily file backups can be large, so consider incremental backups after an initial full backup. Here's a simple full backup approach using tar:

php
#!/bin/bash
# Daily file backup script

BACKUP_DIR="/backups/wordpress"
WP_ROOT="/var/www/wordpress"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/files_backup_${TIMESTAMP}.tar.gz"

# Backup wp-content, wp-config.php, and .htaccess
tar -czf ${BACKUP_FILE} \
  -C ${WP_ROOT} wp-content/ \
  -C ${WP_ROOT} wp-config.php \
  -C ${WP_ROOT} .htaccess

# Keep only the last 7 full backups
find ${BACKUP_DIR} -name "files_backup_*.tar.gz" -mtime +7 -delete

echo "File backup completed: ${BACKUP_FILE}"

On larger sites with frequent uploads, you might keep full backups weekly and incrementals daily. Tools like rsync with timestamped snapshots give you this flexibility efficiently.

Both database and file backups should be logged. Track when they ran, their size, and whether they completed successfully. A failed backup is worse than no backup—you think you're covered when you're not.

Store off-site

Backups sitting on the same server as your live WordPress installation are better than nothing, but they're not protection against server hardware failure, a catastrophic breach, or a hosting provider going down. Off-site storage is non-negotiable for production sites.

Cloud storage is the practical default. AWS S3, Google Cloud Storage, Azure Blob Storage, or even Backblaze B2 all work well. They're cheap (S3 costs pennies per GB per month), reliable, and geographically distributed.

Here's a bash script that uploads compressed backups to S3 using the AWS CLI:

php
#!/bin/bash
# Upload backups to AWS S3

BACKUP_DIR="/backups/wordpress"
S3_BUCKET="s3://my-wordpress-backups"
TIMESTAMP=$(date +"%Y%m%d")

# Upload database backup
if [ -f "${BACKUP_DIR}/db_backup_*.sql.gz" ]; then
  aws s3 cp ${BACKUP_DIR}/db_backup_*.sql.gz \
    ${S3_BUCKET}/database/${TIMESTAMP}/ \
    --storage-class STANDARD_IA
fi

# Upload file backup
if [ -f "${BACKUP_DIR}/files_backup_*.tar.gz" ]; then
  aws s3 cp ${BACKUP_DIR}/files_backup_*.tar.gz \
    ${S3_BUCKET}/files/${TIMESTAMP}/ \
    --storage-class STANDARD_IA
fi

echo "Backups uploaded to S3"

Use STANDARD_IA (infrequent access) storage class for older backups. You pay less per GB but slightly more per access—fine for disaster scenarios where speed isn't your priority. Set a lifecycle policy to delete backups older than 90 days if your risk tolerance allows, or keep them indefinitely if compliance requires it.

Credentials matter here. Use IAM roles and policies that grant only S3 upload permissions to the specific bucket. Never hardcode AWS access keys in scripts. If your server is compromised, you don't want attackers able to delete your backups.

Alternatives to cloud storage: redundant on-premises NAS drives, off-site physical servers at another data center, or hybrid approaches (local for quick recovery, cloud for disaster recovery). The point is simple: backups must exist on infrastructure you don't control, in a location separate from your primary server.

Test restores

This is where most teams stumble. A backup that's never been tested is a backup you haven't actually taken. Restore testing is the only way to confirm your backup strategy actually works.

Test frequency should match your risk tolerance. On mission-critical sites (e-commerce, SaaS platforms), we test restores monthly. For lower-risk sites, quarterly is reasonable. At minimum, test once before going live with any new backup process.

Test environment should mirror production as closely as possible. Ideally, it's a separate staging server where you can safely break things. Here's a practical restore workflow:

  1. Download the latest database backup from off-site storage to a staging server
  2. Download the latest file backup
  3. Extract files to a temporary directory on staging
  4. Create a fresh WordPress database on staging
  5. Import the backed-up database
  6. Replace staging wp-content with backed-up wp-content
  7. Update staging wp-config.php to use staging database credentials
  8. Load the site in a browser and verify it works

Here's a bash script to automate database restore on staging:

php
#!/bin/bash
# Restore WordPress database from backup

BACKUP_FILE="$1"  # Path to backup SQL file
DB_NAME="wordpress_staging"
DB_USER="wp_staging"
DB_PASSWORD="staging_password"
DB_HOST="localhost"

if [ -z "$BACKUP_FILE" ]; then
  echo "Usage: restore_db.sh /path/to/backup.sql.gz"
  exit 1
fi

# Decompress if needed
if [[ $BACKUP_FILE == *.gz ]]; then
  gunzip -c "$BACKUP_FILE" | \
    mysql -h ${DB_HOST} -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME}
else
  mysql -h ${DB_HOST} -u ${DB_USER} -p${DB_PASSWORD} ${DB_NAME} < "$BACKUP_FILE"
fi

echo "Database restore completed"

Document what you test for: posts and pages display correctly, user accounts work, custom post types sync properly, image uploads appear in the media library, plugin settings persist, and the site performs at expected speed. Write a simple checklist and use it every time.

After restoring, also verify that your backup files aren't corrupted by checking their integrity before relying on them. Test extraction and file permissions.

Keep before-update snapshots

Major WordPress updates, plugin upgrades, or theme changes carry risk. A full backup before these operations is emergency insurance. This isn't instead of daily backups—it's in addition to them.

When to snapshot:

  • Before WordPress core updates (especially major version jumps)
  • Before updating plugins with significant dependency changes
  • Before deploying custom code to production
  • Before running database migrations
  • Before bulk user or content imports

A snapshot should capture the exact state of your database and files at that moment. For pre-update snapshots, we recommend a manual backup rather than relying on an automated schedule that might have already moved past the point you need.

Here's a pre-update snapshot workflow:

php
#!/bin/bash
# Pre-update snapshot before plugin/WordPress update

BACKUP_DIR="/backups/wordpress"
WP_ROOT="/var/www/wordpress"
UPDATE_TYPE="$1"  # e.g., "wordpress-update", "plugin-woocommerce"
SNAPSHOT_DIR="${BACKUP_DIR}/snapshots/$(date +%Y%m%d_%H%M%S)_${UPDATE_TYPE}"

mkdir -p ${SNAPSHOT_DIR}

# Full database snapshot
mysqldump -u wp_user -p wordpress_db | gzip > ${SNAPSHOT_DIR}/database.sql.gz

# Full files snapshot
tar -czf ${SNAPSHOT_DIR}/wp-content.tar.gz -C ${WP_ROOT} wp-content/

# Copy critical config
cp ${WP_ROOT}/wp-config.php ${SNAPSHOT_DIR}/wp-config.php
cp ${WP_ROOT}/.htaccess ${SNAPSHOT_DIR}/.htaccess 2>/dev/null

# Log snapshot metadata
echo "Update type: ${UPDATE_TYPE}" > ${SNAPSHOT_DIR}/README.txt
echo "Snapshot time: $(date)" >> ${SNAPSHOT_DIR}/README.txt
echo "WordPress version: $(grep 'wp_version' ${WP_ROOT}/wp-includes/version.php)" >> ${SNAPSHOT_DIR}/README.txt

# Upload to S3
aws s3 cp ${SNAPSHOT_DIR} s3://my-wordpress-backups/snapshots/$(basename ${SNAPSHOT_DIR})/ --recursive

echo "Pre-update snapshot created and uploaded: ${SNAPSHOT_DIR}"

Label snapshots clearly with the update type and timestamp. When an update breaks something, you have exactly what you need to roll back—not yesterday's backup, but the state from 10 minutes before the update started.

Common mistakes

Backup location doesn't match restore assumptions. If your backups use absolute paths but your restore script expects relative paths, or if you backup from a different user account, restores can fail silently. Test with the exact same scripts and paths you'll use in a real emergency.

Compression without verification. A corrupted backup file that's been compressed looks fine on disk—until you try to restore from it. Verify backup integrity with checksum commands after creation: md5sum backup.sql.gz. Store checksums alongside backups and re-verify them occasionally.

Ignoring database table prefixes and custom settings. WordPress allows custom table prefixes and stores configuration in wp-config.php. If your restore script assumes default prefixes or hardcoded settings, it'll fail on sites with non-standard setups. Make your restore scripts parameterized and environment-aware.

No retention policy. Backups accumulate storage costs fast. Without a clear policy (e.g., keep 30 daily, 12 weekly, 12 monthly), your S3 bucket swells and your bill with it. Automate deletion of old backups and document your retention policy in code comments.

Restoring to production without staging validation first. Always restore to staging and verify functionality before even considering a production restore. Production is live traffic and real data—it's the last place to discover your backup is incomplete or your restore process is broken.

Key takeaways

  • Automate daily backups of both database and files using scheduled cron jobs; log every backup for auditability
  • Store backups off-site (AWS S3, cloud storage, or geographically separate infrastructure) to survive server failure or catastrophic breach
  • Test restore procedures monthly or quarterly in staging; a backup you've never successfully restored from is worse than useless
  • Create full snapshots before major updates and deployments; label them clearly with type and timestamp so you can roll back quickly
  • Verify backup integrity with checksums and confirm file permissions and database table structure post-restore
  • Document retention policies in code and automate cleanup to manage storage costs and compliance requirements
  • Use IAM policies and secure credentials for off-site uploads; never hardcode access keys in scripts
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles