Backups and Disaster Recovery

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

WordPress core has no backup feature. A site is two things stored in two places, a MySQL or MariaDB database and a directory of files, and a usable backup captures both, at the same moment, somewhere the failure that destroys the site cannot reach. A backup is only proven by a restore, and the time that restore takes is the real recovery time. This page is for administrators and site owners: it defines what to copy, gives a complete backup script and a complete restore script for the Lumen Press site, measures the restore, and covers off-site storage, backup plugins, host backups and failover.

This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the version to run in production), PHP 7.4 minimum and 8.3+ recommended, WP-CLI 2.12.0 and MariaDB 11.8, and checked on 2026-10-28. Both scripts were run against a Lumen Press test site (3,000 lp_book posts, 500 posts, 60 cover images with their generated sizes, about 82 MB of uploads) and restored into a new, empty database server and an empty web root.

What a WordPress backup must contain

The official backup guide splits the job into the database (content, users, settings) and the files (core, themes, plugins, uploads, wp-config.php, .htaccess). Neither half is useful without the other: uploads without the database have no posts pointing at them, and a database without wp-content references themes, plugins and images that are gone.

Part Where Changes Backup approach

Database

The database named by DB_NAME; all tables with the $table_prefix (12 on a single site, more on Multisite and with plugins that add tables)

Every edit, comment, login, cron run and order

wp db export or mariadb-dump/mysqldump with --single-transaction; the most frequent copy

Uploads

wp-content/uploads/ (wp-content/uploads/sites/<id>/ for Multisite subsites)

Every media upload and every regenerated image size

Archive or incremental sync (rsync, object storage); usually the largest part

Plugins, themes, must-use plugins, languages

wp-content/plugins/, wp-content/themes/, wp-content/mu-plugins/, wp-content/languages/

On installs and updates

Archive with wp-content; custom code (lumen-catalog, lumen, lumen-child) also lives in version control

Drop-ins

wp-content/object-cache.php, advanced-cache.php, db.php, maintenance.php, sunrise.php

Rarely

Archive with wp-content; a missing object-cache.php silently disables the object cache after a restore

wp-config.php

The WordPress root, or one level above it

Rarely

Archive; it holds database credentials and the authentication keys and salts, so the archive is a secret

.htaccess

The WordPress root (Apache only)

On permalink changes and security rules

Archive; nginx sites keep their server block in configuration management instead

Core files

Everything else under the root (wp-admin/, wp-includes/, root PHP files)

Only on core updates

Not needed: download the same version again; record the version with the backup

Some paths under wp-content are regenerable or temporary and only make backups larger and slower:

  • wp-content/cache/, written by page-cache plugins and rebuilt on the next requests.

  • wp-content/upgrade/, the scratch directory for updates, and wp-content/upgrade-temp-backup/, where WordPress 6.3+ moves the previous version of a plugin or theme during an update so it can roll back a failed update. It is emptied after the update; it is not a backup and must not be treated as one.

  • wp-content/debug.log, when WP_DEBUG_LOG is on (it may also contain data that should not be kept).

  • Archives written by backup plugins into wp-content or wp-content/uploads (for example wp-content/updraft/ or wp-content/ai1wm-backups/). Archiving them backs up the backups, and storing them under the web root can make them downloadable.

A complete disaster recovery also needs what is outside the WordPress directory: the web server and PHP configuration, TLS certificates (or the means to reissue them), system cron entries such as the one that replaces WP-Cron, DNS records and the credentials for the off-site storage. Keeping those in configuration management or infrastructure-as-code is cheaper than backing them up by hand. A WXR export from Tools > Export is not a backup either: it omits settings, password hashes, themes, plugins and media files; see Importing, Exporting and Migrating.

RPO, RTO and a backup plan

Two numbers turn "regular backups" into a plan:

  • Recovery point objective (RPO): the most data, measured in time, that the site can afford to lose. A nightly backup means an RPO of up to 24 hours: an editor’s work from the afternoon and every comment since the last run are gone.

  • Recovery time objective (RTO): how long the site may be down before it is serving again. It includes noticing the failure, getting a server, fetching the backup, restoring and checking, not just the import.

The official guide suggests weekly backups for small, quiet sites, daily backups for busy ones, a backup before every upgrade or move, and keeping at least three to five recent backups in several places. Frequency should follow the RPO and the part that changes: for Lumen Press the database changes all day and is small, the uploads change a few times a day and are large, and code changes only through deployments.

Lumen Press data Target RPO Copy Retention

Database

1 hour

Hourly wp db export, plus the nightly full backup

48 hourly copies, 30 daily, 12 monthly

Uploads and wp-content

24 hours

Nightly archive (or a continuous sync to object storage)

30 daily, 12 monthly

Custom code (lumen-catalog, lumen, lumen-child)

0

Git repository and release artefacts

Repository history

Server configuration

On change

Infrastructure-as-code repository

Repository history

The 3-2-1 rule is a useful minimum: three copies of the data, on two different kinds of storage, one of them off-site (another provider or region, with separate credentials). The target RTO for Lumen Press is one hour; the restore test later on this page shows how much of that the restore itself consumes.

Backing up the database

wp db export

wp db export runs mysqldump or mariadb-dump, whichever the server provides, with the credentials from wp-config.php, so they never appear on the command line or in the process list. Any option it does not know is passed to the dump tool, which is how --single-transaction reaches it. Writing to - sends the dump to standard output for compression in the same pipe. Without a file name it writes {dbname}-{Y-m-d}-{random-hash}.sql in the current directory, which is often the web root.

# Consistent snapshot of every table, compressed while it is written
wp db export - --single-transaction --quick --default-character-set=utf8mb4 \
  | gzip > /var/backups/lumen/lumen-db-$(date -u +%Y%m%dT%H%M%SZ).sql.gz

# Only the WordPress tables, when the database is shared with another application
wp db export lumen-wp-only.sql --single-transaction \
  --tables="$(wp db tables --all-tables-with-prefix --format=csv)"

By default wp db export dumps every table in the database, not only those with the WordPress prefix. On the test server, a database holding the 12 WordPress tables and one table of another application produced a dump with 13 CREATE TABLE statements; with --tables="$(wp db tables --all-tables-with-prefix --format=csv)" it produced 12. It also adds --no-tablespaces unless --include-tablespaces is given, so the database user does not need the PROCESS privilege. wp db import is the reverse: it reads a file or standard input (-), turns off unique and foreign-key checks during the import (--skip-optimization keeps them) and adapts the session SQL mode for WordPress.

mariadb-dump and mysqldump directly

On hosts without WP-CLI, or for a backup user with read-only privileges, call the dump tool directly. Keep the credentials in an option file readable only by the backup user rather than in -p<password> on the command line.

# /etc/lumen-backup.cnf  (chmod 600, owned by the backup user)
[client]
host=db.lumenpress.internal
user=lumen_backup
password=<backup-user-password>
mariadb-dump --defaults-extra-file=/etc/lumen-backup.cnf \
  --single-transaction --quick --routines --default-character-set=utf8mb4 \
  lumen | gzip > /var/backups/lumen/lumen-db.sql.gz

The options matter:

  • --single-transaction starts a transaction before reading, so InnoDB tables (the engine WordPress creates by default) are dumped as one consistent snapshot while the site keeps writing. It does not protect MyISAM or MEMORY tables, which some old plugins still create, and other connections must not run ALTER TABLE, CREATE TABLE, DROP TABLE, RENAME TABLE or TRUNCATE TABLE during the dump; a plugin update that alters its tables while the backup runs can make the dump fail or read wrong data. Schedule backups away from deployments. Without the option the tool locks each table as it reads it, so a post and its metadata can come from different moments.

  • --quick streams rows instead of buffering whole tables in memory (it is on by default in mariadb-dump).

  • --routines adds stored procedures and functions, which WordPress does not use but some plugins do. Triggers are dumped with their tables by default.

  • --default-character-set=utf8mb4 matches the character set WordPress uses since 4.2; mariadb-dump defaults to it.

The official database page shows mysqldump --add-drop-table without --single-transaction; adding it is what makes a dump of a live site consistent. Since MariaDB 11.0 the mysqldump name is a deprecated symlink to mariadb-dump, and the MariaDB Docker image no longer ships it. A mariadb-dump from current MariaDB versions also starts with a /*M!999999\- enable the sandbox mode */ line. The MariaDB documentation says older mariadb clients and the MySQL mysql client cannot interpret it; the MySQL 8.4 client imported such a dump without error on the test server, but when restoring into MySQL with an older client, remove the first line (sed 1d) or import with wp db import, which passes the same file to the client in the target environment.

phpMyAdmin

Shared hosts often offer only phpMyAdmin (through cPanel or Plesk). Its Export tab has a Quick mode (all tables, SQL format) and a Custom mode, where the official guide recommends selecting the WordPress tables, gzip or zip output, SQL format and "Add DROP TABLE / VIEW / PROCEDURE / FUNCTION / EVENT / TRIGGER statement". Restoring is the Import tab of the target database. phpMyAdmin runs inside PHP, so large databases hit max_execution_time, memory_limit and the upload_max_filesize limit on import; the official guide warns that it cannot handle large databases and points to the command line or MySQL Workbench. It cannot be scheduled, so it is a tool for an occasional manual copy, not a backup strategy. Older books present it as the main way to back up the database; on any host with SSH, WP-CLI or the dump tool is faster and repeatable.

Larger databases and point-in-time recovery

A logical dump is simple and portable but slow to restore for multi-gigabyte databases, because the import rebuilds every index. Beyond a few gigabytes, physical backups (mariadb-backup for MariaDB, the managed snapshots of a cloud database service) restore much faster. For an RPO of minutes rather than hours, keep the server’s binary log and replay it on top of the last full backup up to the moment before the failure (point-in-time recovery). The transaction model they rely on is described in Transaction Control.

On Amazon RDS or Aurora, the automated backups, point-in-time recovery window, final snapshot and deletion protection are instance settings; see Managed Database: RDS and Aurora. In a cluster, the uploads live in a versioned bucket and the code in an image registry, so a backup plan covers those too; see Media Offload: S3, EFS and CDN.

Backing up the files

The files part is wp-content, wp-config.php and .htaccess. Core is better downloaded again at the recorded version, which also guarantees that the restored core is unmodified. A compressed tar archive keeps owners, permissions and timestamps and can exclude the regenerable paths listed earlier:

tar -C /var/www/html -czf /var/backups/lumen/files.tar.gz \
  --exclude='wp-content/cache' \
  --exclude='wp-content/upgrade' \
  --exclude='wp-content/upgrade-temp-backup' \
  --exclude='wp-content/debug.log' \
  wp-content wp-config.php .htaccess

On the test site the uploads directory held 82 MB of JPEG files plus a 29 MB page cache. The archive was 17.5 MB and contained 866 entries, none of them under wp-content/cache/ and no debug.log. Already-compressed media (JPEG, WebP, AVIF, MP4) barely shrink, so for sites with tens of gigabytes of uploads an incremental copy (rsync to a backup host, or an object-storage sync with versioning) is far cheaper than a nightly full archive, and the archive then covers only code, configuration and drop-ins. FTP/SFTP downloads and sync tools such as WinSCP, which the official guide also lists, work for small sites but are manual.

A complete backup script

The script below is what the Lumen Press server runs every night. It exports the database first (as the official guide recommends), archives the files, records what is needed to rebuild the site, writes and immediately verifies SHA-256 checksums, tests the gzip stream, and deletes local backup directories older than the retention period. Archives are created with umask 077 because they contain wp-config.php and personal data. It needs WP-CLI, GNU tar, gzip, sha256sum and find, and runs as the user that owns the site files.

#!/usr/bin/env bash
# lumen-backup.sh: full backup of the Lumen Press site (database + files).
set -euo pipefail

SITE_PATH="${SITE_PATH:-/var/www/html}"          # WordPress root (where wp-config.php lives)
BACKUP_ROOT="${BACKUP_ROOT:-/var/backups/lumen}"  # local staging area; copy off-site afterwards
RETAIN_DAYS="${RETAIN_DAYS:-14}"                  # local retention, in days

STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
DEST="$BACKUP_ROOT/$STAMP"
umask 077                                         # archives contain secrets: owner-only
mkdir -p "$DEST"

# 1. Database first: a consistent InnoDB snapshot, streamed and compressed.
wp --path="$SITE_PATH" db export - --single-transaction --quick \
  --default-character-set=utf8mb4 | gzip -6 > "$DEST/db.sql.gz"

# 2. Files: wp-content (minus regenerable data) plus the root files that are not core.
tar -C "$SITE_PATH" -czf "$DEST/files.tar.gz" \
  --exclude='wp-content/cache' \
  --exclude='wp-content/upgrade' \
  --exclude='wp-content/upgrade-temp-backup' \
  --exclude='wp-content/debug.log' \
  --exclude='wp-content/uploads/*/backups' \
  wp-content wp-config.php $( [ -f "$SITE_PATH/.htaccess" ] && echo .htaccess )

# 3. Facts needed to rebuild: core version, locale, home URL, active plugins.
{
  echo "core_version=$(wp --path="$SITE_PATH" core version)"
  echo "locale=$(wp --path="$SITE_PATH" option get WPLANG 2>/dev/null || true)"
  echo "home=$(wp --path="$SITE_PATH" option get home)"
  echo "table_prefix=$(wp --path="$SITE_PATH" config get table_prefix)"
  echo "plugins=$(wp --path="$SITE_PATH" plugin list --status=active --field=name | paste -sd, -)"
} > "$DEST/manifest.txt"

# 4. Checksums, verified immediately (catches a truncated write).
( cd "$DEST" && sha256sum db.sql.gz files.tar.gz manifest.txt > SHA256SUMS \
    && sha256sum -c SHA256SUMS >/dev/null )
gzip -t "$DEST/db.sql.gz"

# 5. Retention: delete local backup directories older than RETAIN_DAYS.
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -name '20*Z' \
  -mtime +"$RETAIN_DAYS" -exec rm -rf {} +

echo "Backup written to $DEST"
du -sh "$DEST"/*

A run on the test site took about 5 to 11 seconds and printed:

Backup written to /var/backups/lumen/20261028T104047Z
4.0K	/var/backups/lumen/20261028T104047Z/SHA256SUMS
84.0K	/var/backups/lumen/20261028T104047Z/db.sql.gz
17.5M	/var/backups/lumen/20261028T104047Z/files.tar.gz
4.0K	/var/backups/lumen/20261028T104047Z/manifest.txt

set -o pipefail is essential: without it a failed wp db export piped into gzip still exits 0 and leaves a valid, empty-looking db.sql.gz. The retention step was checked by adding a directory dated 2026-10-01; the next run with RETAIN_DAYS=14 removed it and kept the two runs from 2026-10-28. Retention on the local disk only bounds disk usage: the long-term copies (monthly, yearly) belong in the off-site store, with its own lifecycle rules.

Schedule the script with the system cron, not with WP-Cron. WP-Cron only runs when someone visits the site, so on a quiet night a WP-Cron backup can be hours late, and it runs inside a web request with PHP time limits (see WP-Cron and Scheduled Tasks).

# /etc/cron.d/lumen-backup
# Nightly full backup at 02:15 UTC, hourly database-only copy.
15 2 * * *  www-data  SITE_PATH=/var/www/lumen /usr/local/bin/lumen-backup.sh >> /var/log/lumen-backup.log 2>&1
5 * * * *   www-data  wp --path=/var/www/lumen db export - --single-transaction | gzip > /var/backups/lumen/hourly-$(date -u +\%H).sql.gz

The % signs are escaped because cron treats an unescaped % as a newline. Naming the hourly files by hour (hourly-00 to hourly-23) gives a rolling 24-hour window without a separate clean-up. Monitor both jobs: alert when the newest backup is older than the RPO or when its size drops sharply, which is how a silently failing backup is usually noticed.

Off-site copies and protecting backups

A backup on the same server, disk or account as the site does not survive the failures that matter most: a lost server, a deleted hosting account, ransomware or an attacker with the site’s credentials. After each run, copy the backup directory to storage in another provider or region, with credentials that the web server cannot use to delete it.

# Appended to lumen-backup.sh: copy the new backup to an off-site remote configured in rclone
rclone copy "$DEST" "offsite:lumen-backups/$STAMP" --immutable

Source: rclone copy

--immutable makes rclone refuse to modify files that already exist on the remote. On the storage side, enable object versioning or an object lock (write once, read many) with a retention period, so that a compromised server can add backups but not erase them. Further protections:

  • Encrypt backups that leave the server (an rclone crypt remote, age, gpg), because they contain the authentication salts, database credentials, user emails and password hashes. Keep the decryption key somewhere the disaster cannot reach, such as a password manager, not only on the server.

  • Never store backups under the web root. An archive in wp-content/uploads/ or the document root can be found and downloaded by anyone; deny access to such directories in the web server if a plugin insists on them.

  • Restrict permissions to the backup user (umask 077 in the script; 0600 files, 0700 directories).

  • Treat the backup as personal data under privacy law: retention periods and erasure requests apply to it too.

Restoring

The official order is: restore the files, then import the database, and update wp-config.php if the database credentials changed. The restore script below does exactly that into an empty web root and an empty database: it refuses a backup whose checksums do not match, downloads the core version recorded in the manifest (the package without default themes and plugins), unpacks wp-content, wp-config.php and .htaccess, points wp-config.php at the replacement database server when NEW_DB_* variables are set, imports the database and runs the post-restore checks.

#!/usr/bin/env bash
# lumen-restore.sh: rebuild the Lumen Press site from one backup directory.
set -euo pipefail

BACKUP_DIR="$1"                                   # e.g. /var/backups/lumen/20261028T020000Z
SITE_PATH="${SITE_PATH:-/var/www/html}"

# 1. Refuse to restore a damaged backup.
( cd "$BACKUP_DIR" && sha256sum -c SHA256SUMS )
CORE_VERSION="$(sed -n 's/^core_version=//p' "$BACKUP_DIR/manifest.txt")"

# 2. Core from WordPress.org, at the exact version that wrote the database.
mkdir -p "$SITE_PATH"
php -d memory_limit=512M "$(command -v wp)" core download \
  "https://wordpress.org/wordpress-${CORE_VERSION}-no-content.zip" --path="$SITE_PATH"

# 3. wp-content, wp-config.php and .htaccess from the archive.
tar -C "$SITE_PATH" -xzf "$BACKUP_DIR/files.tar.gz"

# 3b. The replacement database server may differ from the one in the archive.
for c in DB_HOST DB_NAME DB_USER DB_PASSWORD; do
  v="NEW_$c"
  if [ -n "${!v:-}" ]; then wp --path="$SITE_PATH" config set "$c" "${!v}" --quiet; fi
done

# 4. Database: wp-config.php now points at the (new, empty) database.
wp --path="$SITE_PATH" db create 2>/dev/null || true
gunzip -c "$BACKUP_DIR/db.sql.gz" | wp --path="$SITE_PATH" db import -

# 5. Checks and clean-up.
wp --path="$SITE_PATH" core verify-checksums
wp --path="$SITE_PATH" core update-db
wp --path="$SITE_PATH" cache flush
wp --path="$SITE_PATH" rewrite flush
wp --path="$SITE_PATH" option get home

The core package is downloaded by URL because --skip-content cannot be combined with a URL download (WP-CLI 2.12.0 answers "Skip content and locale options are not available for URL downloads"); the -no-content package is the one WordPress.org builds for that purpose. Using the exact version matters: a newer core would run database upgrades on the restored data, and an older one would refuse to work with a database written by a newer version. wp core update-db is a no-op when the versions match, as the output below shows, and finishes the job after a deliberate core upgrade.

The site was restored with the database password changed on the new server:

NEW_DB_HOST=db2.lumenpress.internal NEW_DB_PASSWORD="$LUMEN_DB_PASSWORD" \
  time ./lumen-restore.sh /var/backups/lumen/20261028T104047Z
db.sql.gz: OK
files.tar.gz: OK
manifest.txt: OK
Downloading from https://wordpress.org/wordpress-7.1.3-no-content.zip ...
md5 hash verified: 8afdbea5f8cfd97d3739dae451340ade
Success: WordPress downloaded.
Success: Imported from 'STDIN'.
Success: WordPress installation verifies against checksums.
Success: WordPress database already at latest db version 61833.
Success: The cache was flushed.
Success: Rewrite rules flushed.
https://lumenpress.test
real	0m 10.34s

The restored site had 3,000 lp_book posts, 60 attachments with 360 files in wp-content/uploads/2026/10/, lumen-catalog active, the /%postname%/ permalink structure, and book permalinks such as https://lumenpress.test/books/post-2999/. A backup with one altered byte in files.tar.gz was rejected at the first step (files.tar.gz: FAILED, exit status 1) before anything was written.

After a restore

  • Plugin integrity: wp plugin verify-checksums --all compares plugins from the WordPress.org directory with their published checksums. Custom and commercial plugins have none; for lumen-catalog it prints "Could not retrieve the checksums for version 0.1.0 of plugin lumen-catalog, skipping" and reports "Verified 2 of 3 plugins (1 skipped)", so those plugins are checked against their own repositories instead.

  • Caches: flush the persistent object cache (wp cache flush), any page cache and the CDN, or visitors keep seeing pre-restore pages and the object cache keeps pre-restore options. A Redis object cache does not need to be backed up; it is rebuilt from the database.

  • After a compromise: restore from a backup made before the intrusion, update core, plugins and themes, change all passwords and rotate the authentication keys and salts with wp config shuffle-salts, which logs everyone out (see Security Hardening). Restoring the same vulnerable versions only resets the clock.

  • Different URL: a restore to another domain (staging, a new host name) needs wp search-replace on the database, which handles serialized data; that is a migration, covered in Importing, Exporting and Migrating.

  • Site Health: open Tools > Site Health to confirm loopback requests, scheduled events and the object cache drop-in, then log in, open a post in the editor and load a few front-end pages.

When restoring in place on a running server, put the site into maintenance mode first so that visitors and cron do not write to a half-restored database, and take one more backup of the broken state for later analysis:

wp maintenance-mode activate
wp db export /var/backups/lumen/pre-restore-$(date -u +%Y%m%dT%H%M%SZ).sql --single-transaction
gunzip -c /var/backups/lumen/20261028T104047Z/db.sql.gz | wp db import -
wp cache flush
wp maintenance-mode deactivate

wp maintenance-mode activate writes the .maintenance file in the root, which makes WordPress answer every request with a 503 "Briefly unavailable for scheduled maintenance" page (or wp-content/maintenance.php, if present).

Testing restores and measuring RTO

The manual upgrade instructions say to verify that the backups exist and are usable, and call that essential. A checksum proves the archive was not damaged after it was written, and a backup plugin’s "verification" usually proves the same; neither proves the archive contains what is needed. Only a restore does. Schedule a restore test (monthly for Lumen Press, and after any change to the backup scripts, the hosting or the database version) into an isolated environment, then check content counts, a login, the editor and a few pages, as above.

The test also produces the RTO figure. On the test site the restore took about 10 seconds; the parts that grow with the site are the downloads of the backup from off-site storage (network-bound, roughly proportional to the uploads), the database import (index rebuilds, which dominate above a few gigabytes) and cache warm-up. The parts that do not show in a scripted test are often longer: noticing the outage, getting access to the off-site storage and the decryption key, provisioning a server and changing DNS (whose old records stay cached for their TTL). Write the steps down as a runbook, time a full drill once or twice a year, and compare the total with the RTO; if it does not fit, the answer is a standby environment rather than faster backups.

Backup plugins and host backups

Plugins are the usual choice on shared hosting without SSH. Common ones in the directory are UpdraftPlus and BackWPup (scheduled database and file backups to remote storage), All-in-One WP Migration (whole-site export and import, mostly used for migrations) and WP-DB-Backup (database only, the example used in the official database guide). Jetpack VaultPress Backup is a commercial service with off-site, real-time backups; commercial products change names and owners over time (the BackupBuddy plugin named in one older book is sold under another name today). Whatever the plugin, check where archives are stored (not under the web root, not only on the same server), whether remote-storage credentials sit in the database where a compromise exposes them, that scheduling depends on WP-Cron, that large sites hit PHP time and memory limits, and that a restore works on a clean server.

Host backups (daily snapshots in the hosting panel) are a useful extra copy, and older books rely on them. They are usually kept on the same provider, sometimes the same infrastructure, for a limited time; restoring them may be a support ticket; and they disappear with the account. Find out the retention, whether they can be downloaded, and how long a restore takes, and keep an independent off-site copy as well. The official guide also notes that cPanel’s full account backups are archival and cannot be restored through cPanel itself.

The rollbacks WordPress performs on its own are not backups either. Since 6.3 a failed manual plugin or theme update is rolled back from wp-content/upgrade-temp-backup/, and since 6.6 a plugin auto-update that breaks the site is rolled back, but neither covers content, settings, a bad but successful update or a lost server (see Updates and Auto-Updates).

Disaster recovery and failover

Disaster recovery is the plan for getting the site back after a major failure; failover is switching automatically to a standby component when the active one fails. Failover reduces RTO for infrastructure failures, but it replicates mistakes: a dropped table or a defaced page is copied to the replica within seconds. A standby never replaces backups.

Incident Recovery What limits RPO and RTO

Bad plugin or theme update

Roll back the code (previous release, wp plugin install <slug> --version=<old> --force); restore the database only if the update migrated data

Whether the update changed the schema; whether a pre-update backup exists

Content deleted or corrupted by a user

Restore single posts from the Trash (30 days by default) or revisions; otherwise import the backup into a scratch database and copy the rows back

Backup frequency; time to find the right rows

Hacked site

Restore from a backup made before the intrusion onto a clean server, then patch and rotate credentials

Knowing when the intrusion started; retention long enough to go back that far

Database server lost

Promote a replica, or restore the last dump (plus binary logs) on a new server

Replication lag or dump frequency; dump size

Provider or region lost

Rebuild in another region or provider from off-site backups and infrastructure-as-code, then switch DNS

Off-site copy freshness; DNS TTL; access to keys

For an RTO of minutes, Lumen Press would run a warm standby: a database replica and a synchronised copy of wp-content (or shared object storage for uploads) in a second location, web nodes built from the same images, and a health-checked load balancer or DNS failover in front. The design of stateless web nodes, shared uploads and database replication is covered in Scaling and High Availability; for containerised sites, the volumes that hold the database and wp-content are the data to back up (see Storage: Volumes, Bind Mounts and tmpfs).

On Multisite, the database contains the network tables and one set of tables per site, and uploads are under wp-content/uploads/sites/<id>/, so the whole-network backup above covers every site. Restoring a single subsite means importing its wp_<id>_* tables and its uploads directory while leaving the network tables untouched; see Multisite Networks.

Where older books are outdated

Older advice Current practice

A database backup made with phpMyAdmin is enough before upgrading

The official upgrade instructions require a backup of the database and of all files, including .htaccess, and a check that they are usable. Plugin and theme updates change files, and a restored database without the matching files can be inconsistent.

phpMyAdmin Export is the standard way to back up the database

It still works for small sites, but it cannot be scheduled and fails on large databases. wp db export or mariadb-dump --single-transaction from cron is repeatable and consistent.

Back up wp-content (and the database) and you are done

wp-config.php and .htaccess are also needed, plus drop-ins and the server configuration. Core can be skipped if its version is recorded.

Weekly backups suit most sites

Frequency follows the RPO of each part. A weekly database backup can lose a week of posts, comments and orders; the database is small and cheap to copy often.

Backup plugins verify the integrity of backups

A checksum or a plugin’s check proves the file is intact, not that it restores. Only a restore test proves the backup and gives the RTO.

Daily host backups make your own backups unnecessary

Host backups are a good extra copy but share the provider’s fate and retention limits. Keep an independent, encrypted off-site copy.

mysqldump is the dump tool

On MariaDB 11 the tool is mariadb-dump; mysqldump is a deprecated alias and is missing from the MariaDB container image. WP-CLI uses whichever exists.

Common errors and pitfalls

  • Dump files in the web root. wp db export without a path writes into the current directory, often the document root; a leftover lumen-2026-10-28-xxxx.sql is downloadable by anyone who guesses the name.

  • A pipeline that hides failures. Without set -o pipefail, a failed export piped into gzip exits 0. Verify with gzip -t and alert on unusually small dumps.

  • Character set damage. Importing with a client set to latin1 mangles accented characters and emoji; keep utf8mb4 on both ends (WP-CLI and mariadb-dump use it by default).

  • Missing drop-ins or must-use plugins. A backup of plugins/ and themes/ only loses object-cache.php and mu-plugins/, and the restored site behaves differently from the original without any error.

  • URL changes done with SQL REPLACE. Serialized options and widgets break; use wp search-replace.