A backup that has never been restored is only a theory. A restore drill proves whether your backup contains the right data, can be accessed, and can bring the service back within an acceptable time. The first test restore should not happen during an incident.

Define the restore target

Choose the right scope for this drill:

TargetUse whenRisk
Temporary VPSFirst drill, full stack testNone — disposable
Staging domainFrequent testing, client demosMust not send live emails
Database onlyQuick integrity checkDoes not test file recovery
Single filePermission/config testNarrow scope
Full production restorePlanned migrationHighest risk — do after staging success

For the first drill, always use a temporary server. Do not practice on production.

Inventory what must be restored

For a typical WordPress server, you need:

Essential

  • Database dump (.sql or .sql.gz)
  • wp-content/uploads/ (and all subdirectories)
  • wp-content/themes/ (custom themes)
  • wp-content/plugins/ (all plugins, including premium ones not in the repository)
  • wp-config.php
  • .htaccess or nginx config
  • PHP-FPM pool configuration

Frequently forgotten

  • Cron jobs (system crontab for the web user)
  • SSL certificates or ACME client config (Let’s Encrypt renewal paths)
  • DNS records (export zone file before migration)
  • SMTP credentials and API keys
  • Object cache configuration (Redis/Memcached settings)
  • Custom .ini files and PHP extensions
  • Web server includes and snippet files
  • Monitoring agent config
  • Backup job schedules (so backups resume after restore)

Missing secrets and cron jobs are the most common restore surprises. Check both early.

Step-by-step restore

1. Provision a test server

Create a fresh VPS or local Docker environment. The OS version, PHP version, and MySQL/MariaDB version should match the production server.

2. Install the stack

apt update && apt install nginx php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring php8.3-xml php8.3-zip mariadb-server redis-server certbot

3. Restore the database

# If compressed
gunzip < backup-20260607.sql.gz | mysql -u root -p database_name

# If plain SQL
mysql -u root -p database_name < backup-20260607.sql

If the database is large (>500 MB), consider:

pv backup.sql.gz | gunzip | mysql -u root -p database_name

pv shows progress, which is useful for large restores.

4. Run search-replace for the test domain

wp search-replace 'example.com' 'staging.example.com' --skip-columns=guid

Do not skip this step. If the database still references the production URL, you may accidentally redirect test traffic, generate analytics noise, or trigger live webhooks.

5. Restore files

rsync -a backup/wp-content/uploads/ /var/www/staging.example.com/htdocs/wp-content/uploads/
rsync -a backup/wp-content/themes/ /var/www/staging.example.com/htdocs/wp-content/themes/
rsync -a backup/wp-content/plugins/ /var/www/staging.example.com/htdocs/wp-content/plugins/

For extremely large uploads directories, use rsync with --bwlimit to avoid saturating the network:

rsync -a --bwlimit=50000 backup/wp-content/uploads/ /var/www/staging.example.com/htdocs/wp-content/uploads/

6. Fix file permissions

find /var/www/staging.example.com -type d -exec chmod 755 {} \;
find /var/www/staging.example.com -type f -exec chmod 644 {} \;
chmod 600 /var/www/staging.example.com/wp-config.php
chown -R www-data:www-data /var/www/staging.example.com

7. Reconfigure application secrets

Copy wp-config.php to the test server and update:

define('DB_NAME', 'database_name');
define('DB_USER', 'db_user');
define('DB_PASSWORD', 'test_db_password');
define('DB_HOST', 'localhost');

If the original wp-config.php uses environment variables or a secrets manager, replicate that approach on the test server.

8. Restore web server config

Copy nginx or Apache config and test:

nginx -t
systemctl reload nginx

9. Restore SSL

For Let’s Encrypt, issue a new certificate for the test domain:

certbot --nginx -d staging.example.com

Do not copy production certificates to a test server. Issue new ones.

Measure restore time

Track every phase:

PhaseTimeNotes
Locate backup____Where is it? How long to access?
Download/mount____Network speed or snapshot time
Restore database____Affected by database size
Restore files____Affected by uploads size
Search-replace____Affected by database complexity
Reconfigure services____Affected by documentation quality
Application checks____Automated where possible

This gives you a real Recovery Time Objective (RTO) instead of a guess.

Application checks after restore

  • Homepage loads (check HTML, not just HTTP 200)
  • Admin login works
  • Media files render (check a few image URLs directly)
  • Contact form submits (test mode — do not send to real recipients)
  • WooCommerce product pages load
  • Permalinks work (try a few deep URLs)
  • Cron events are present: wp cron event list
  • Cache can be flushed: wp cache flush
  • Error logs are quiet: tail -20 wp-content/debug.log (if enabled)
  • Backups are configured on the new server

Document gaps immediately

If the drill finds missing data, fix the backup job the same day. Common gaps:

  • Missing uploads: The backup plugin only captured the database
  • Expired API keys: SMTP, CDN, or payment gateway keys that only exist in prod env vars
  • Missing plugins: Premium plugins that exist on disk but not in the backup
  • Old PHP version: The backup assumes a PHP extension that is not available on the test server
  • Undocumented DNS: You cannot recreate the zone because nobody exported it

Do not leave notes like “remember to add uploads later.” Backups drift unless they are actively maintained.

Frequency

Site importanceDrill frequency
Business-critical (ecommerce, membership)Monthly automated restore to staging
Important (lead gen, portfolio)Quarterly
Low-risk (brochure site, blog)Twice a year
After any major changeImmediately

A quarterly drill takes a couple of hours. An incident with no working backup can take days and cost revenue. The maths is straightforward.