Most WordPress sites have a backup. Far fewer have one that is stored somewhere other than the server it protects, keeps more than the last copy, and has ever been restored by anyone. The difference matters on exactly one day, and on that day it is the only thing that matters. This guide is the automated WordPress backup system setup we put in place for clients through our automated WordPress backup system setup service: what it has to do, the three ways to build it, the scripts we actually run, and how to prove it works before you need it.
It is for the owner or CTO who suspects the current “the backup plugin is installed” answer is not good enough, and wants to know what good looks like before deciding whether to build it in-house or have it done.
what a backup system has to do
- Offsite. A backup on the same disk, or in the same hosting account, as the site dies with the site. Host account suspended, disk corrupted, ransomware in the web root: all three take local backups with them.
- Versioned. One copy, overwritten nightly, means a hack or a bad migration you notice on Thursday has already overwritten Tuesday’s clean copy. You need a retention policy: daily for two weeks, weekly for two months and monthly for a year is a sane default.
- Complete. Database and files. Files means
wp-content(uploads, plugins, themes, mu-plugins),wp-config.php,.htaccessor the Nginx config, and anything outside the web root the site depends on (cron entries, environment files, TLS certificates). - Frequent enough. Define the recovery point objective in business terms. A brochure site can lose a day. A WooCommerce store losing a day of orders means a refund and an apology per order; it needs the database hourly.
- Tested. A restore that has never been rehearsed is a hypothesis.
- Immutable. The credentials on the server that write backups must not be able to delete them, so an attacker who owns the server cannot own the backups too.
three ways to build it
host-level snapshots
EBS or Lightsail snapshots on AWS, JetBackup on cPanel hosts, the daily backup included with managed WordPress hosting. Fast, whole-server, and easy to restore in full. Weak on granularity (restoring one table means restoring the whole snapshot somewhere else and extracting it), usually stored in the same account, and gone the day you leave the host.
plugin-level backups
UpdraftPlus, WPvivid, BlogVault, Jetpack VaultPress, Duplicator. Configurable from wp-admin, able to send to S3, Google Drive or Dropbox, and non-technical staff can trigger a restore. The costs: they run inside PHP under the web server’s time and memory limits, so they struggle with sites above a few gigabytes; they run on WP-Cron, which only fires when someone visits the site; and their working directory (wp-content/updraft and similar) is inside the web root, where a misconfigured server can expose it. BlogVault and VaultPress avoid most of this by doing the work off-server, at a monthly cost.
server-level scripts
WP-CLI dumps the database, a deduplicating backup tool (restic, Borg or rclone) ships the files and the dump to object storage, system cron runs it, and a health-check endpoint confirms it ran. This is what we deploy wherever we have shell access, because it has no size limit, does not depend on WordPress being healthy, and the retention and immutability are properties of the storage bucket rather than a plugin setting.
| Host snapshots | Plugin | Server script | |
|---|---|---|---|
| Works when WordPress is broken | Yes | No | Yes |
| Large sites (10 GB and up) | Yes | Unreliable | Yes |
| Restore a single table or file | Awkward | Some | Yes |
| Independent of the host account | No | Yes | Yes |
| Non-technical restore | Console only | Yes | No |
| Needs shell access | No | No | Yes |
The combination we recommend most often is host snapshots (for a fast whole-server rollback) plus a server-level script to an S3 bucket in a separate AWS account (for the offsite, versioned, immutable copy). A plugin is fine as the sole method only on small sites where nobody has shell access, and even then, send it offsite.
the server-level automated WordPress backup system setup we deploy
storage: S3 with versioning and Object Lock
Create the bucket in an account that the web server has no other access to. Turn on versioning, add a lifecycle rule that moves objects older than 30 days to S3 Glacier Instant Retrieval and expires them after 400 days, and enable Object Lock in compliance mode for 30 days so that even a compromised key cannot delete recent backups. Backblaze B2 or Wasabi work the same way if AWS is not in the picture.
credentials: write-only
The IAM user or instance role on the server gets PutObject, GetObject and ListBucket on that one bucket, and nothing else. No DeleteObject. Pruning old snapshots is done by the lifecycle rule and by a separate credential used from a different machine.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ],
"Resource": [
"arn:aws:s3:::example-com-backups",
"arn:aws:s3:::example-com-backups/*"
]
}
]
}
the script
restic deduplicates, encrypts client-side and talks to S3 natively. Uploads that were already offloaded to S3 by a media plugin are excluded, because they live in a versioned bucket of their own. Cache directories are excluded. The database dump uses --single-transaction so it is consistent without locking tables on a live store.
#!/usr/bin/env bash
# /usr/local/bin/wp-backup.sh (run by cron as the site user)
set -euo pipefail
SITE=/var/www/example.com
DUMP=/var/backups/example.com/db.sql
export RESTIC_REPOSITORY="s3:s3.eu-west-1.amazonaws.com/example-com-backups/restic"
export RESTIC_PASSWORD_FILE=/etc/restic/example-com.pass
export AWS_DEFAULT_REGION=eu-west-1 # credentials come from the instance role
mkdir -p "$(dirname "$DUMP")"
wp db export "$DUMP" --path="$SITE" --single-transaction --add-drop-table --quiet
if [[ "${1:-}" != "--db-only" ]]; then
restic backup "$SITE" "$DUMP" /etc/nginx/sites-available/example.com
--exclude="$SITE/wp-content/cache"
--exclude="$SITE/wp-content/uploads/cache"
--exclude="$SITE/wp-content/updraft"
--tag "full-$(date +%F-%H%M)"
else
restic backup "$DUMP" --tag "db-$(date +%F-%H%M)"
fi
# tell the monitor we finished; the monitor alerts if this stops arriving
curl -fsS -m 10 --retry 3 "https://hc-ping.com/YOUR-UUID" >/dev/null
# /etc/cron.d/wp-backup
# full files + database nightly, database only hourly on stores
30 2 * * * www-data /usr/local/bin/wp-backup.sh
15 * * * * www-data /usr/local/bin/wp-backup.sh --db-only
# retention, run from the ops machine with a credential that CAN delete
restic forget --keep-hourly 48 --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune
The --db-only branch means that on a store, hourly database snapshots keep the recovery point under an hour without re-scanning 20 GB of uploads every time. restic only uploads changed blocks anyway, so the nightly full run is cheap after the first one.
WooCommerce and other high-change sites
Stores and membership sites change the database constantly and the files rarely. Three adjustments:
- Hourly database backups as above, or enable MySQL binary logging with 48-hour retention on your own server (RDS does this automatically for point-in-time recovery) so you can restore to a specific minute.
- Exclude tables that are pure noise and large:
wp_actionscheduler_logs,wp_wc_admin_note_actions, and any plugin’s own logging or statistics tables.wp db export --exclude_tables=wp_actionscheduler_logshandles it. Most of the “backup is too slow” tickets we see trace to one bloated table; the database cleanup checklist is worth running before you size the backup, or our WordPress database optimization service can do it as part of the same job. - Export the payment gateway, tax and shipping settings separately with
wp option get, because these are the settings that take longest to reconstruct by hand if a restore is partial.
testing the restore, on a schedule
The test is a real restore to a staging environment, scripted so it takes minutes and actually happens. Monthly is the minimum; on stores we do it after every retention change and before every major WordPress update.
# restore the latest snapshot to staging
restic restore latest --target /srv/restore --path /var/www/example.com
rsync -a --delete /srv/restore/var/www/example.com/ /var/www/staging.example.com/
restic restore latest --target /srv/restore --path /var/backups/example.com/db.sql
wp db import /srv/restore/var/backups/example.com/db.sql --path=/var/www/staging.example.com
wp search-replace 'https://example.com' 'https://staging.example.com' --all-tables --path=/var/www/staging.example.com
wp db check --path=/var/www/staging.example.com
wp core verify-checksums --path=/var/www/staging.example.com
curl -sI https://staging.example.com/ | head -1 # expect HTTP/2 200
Record the time it took and the number of manual steps. That number is your recovery time objective, and if it is not acceptable, fix it now rather than during an outage. Monitoring is the other half: a health-check ping (healthchecks.io, Cronitor, or a CloudWatch metric filter) that alerts when the backup does not report in, plus a weekly restic check --read-data-subset=5% so silent corruption in the repository is found early.
the mistakes we clean up most often
- Backups stored in
wp-content/on the same server, sometimes publicly downloadable. - One nightly copy, no versions, so the clean state was overwritten before anyone noticed the hack. This is why recovery so often has to be done by hand; see the exact process a hacked WordPress site recovery specialist follows.
- Plugin backups that time out at 60 percent on every run, emailing a warning to a mailbox nobody reads.
- The S3 key on the server has full access to the bucket, so a compromised server can delete the backups.
- Database only, no files, because “the theme is in Git”. The uploads were not.
- A move to a new host (including an AWS hosting migration for WordPress) where nobody re-pointed the backup job at the new server.
- Restores never tested, and the first real attempt reveals the restic password file only ever existed on the old server.
when to do this yourself vs hire someone
Do it yourself if you have shell access, you are comfortable with cron, IAM policies and one CLI tool, and the site is small enough that a plugin would also work as a fallback. The scripts above are complete; expect a few hours to set up and test.
Hire someone if the site is a store, if it is over a few gigabytes, if nobody on the team has shell access, or if you need the write-only credentials, Object Lock and monitoring done correctly and documented so the next person can find the password file. An automated WordPress backup system setup is a one-time, fixed-scope job, and it fits naturally alongside an ongoing maintenance and support plan where the monthly restore test is part of the routine.
get a backup you have actually seen restored
Tell us where the site is hosted, roughly how large it is, and how much data you can afford to lose. Within 24 hours you get a fixed price for the automated WordPress backup system setup, including the first restore test with you watching. Contact us and we will schedule it.




