Self-Hosted CMS Backup Strategy: A Practical Guide for 2026
DB dumps, media sync, encrypted offsite copies, and the restore drill nobody runs
TL;DR: A self-hosted CMS without a tested backup is a self-hosted CMS waiting to lose data. The fix is small: nightly DB dumps + weekly media snapshots + monthly restore drills + offsite copy. This guide covers the practical setup — what to back up, how often, where to store it, and the restore drill nobody runs until they need it.
The honest version of every self-hosted CMS horror story is the same. The site was fine for two years. Then one Tuesday a deploy broke a migration, a junior dev ran mysql ... < schema.sql against the wrong database, and 18 months of blog posts vanished. The backup existed. It hadn't been tested. It was missing two columns that had been added six months earlier.
You can avoid this with about three hours of one-time setup. Less than the time it took you to write your last blog post. This article is the operational manual.
What "Backup Strategy" Actually Means for a CMS
A backup strategy for a self-hosted CMS is three independent layers of protection plus a rehearsal you actually run. The layers are: database dumps, file/media snapshots, and offsite copies. The rehearsal is a quarterly restore drill where you delete a test environment and rebuild from backup end-to-end.
Most teams have one or two of the layers. Almost none run the drill. That gap is where data losses happen.
The 3-2-1 rule from traditional IT applies here word for word: 3 copies of the data, on 2 different storage types, with 1 copy offsite. For a CMS, that translates to: live DB, local backup, S3-compatible remote. Three copies. Local disk + remote object storage = two types. Remote = offsite. Done.
What's in a CMS Backup (And What Most Guides Miss)
A CMS backup that only covers the database is a half-backup. Five things live outside the database in most self-hosted CMS installs:
- The database — posts, pages, users, settings, media metadata. Obvious.
- Uploaded media — images, PDFs, videos. Stored on disk under
storage/app/public/or similar. Lose these and your posts have broken<img>tags. - The
.envfile — DB credentials, API keys, mail config. Recreate from a backup or you'll spend a day re-finding every key. storage/framework caches and logs — usually NOT critical, but if you have queued jobs instorage/framework/jobs/you may want them.- Generated public assets —
public/build/, hero images, sitemaps. Most are regenerable, but only if your deploy still works.
A complete backup hits 1, 2, and 3 at minimum. Items 4 and 5 are optional — most teams skip them because they regenerate on next deploy.
Database vs Media — Different Backup Cadences
These two should NOT have the same backup frequency. Here's why:
| Asset type | Change rate | Recommended cadence | Reason |
|---|---|---|---|
| Database | High (every new post, comment, settings tweak) | Nightly + on-deploy | Cheap to dump, expensive to lose |
| Media files | Low (occasional new uploads) | Weekly + on-bulk-upload | Backups are larger, change rate is lower |
| .env | Rare (config changes) | On every change, version it | Tiny, must survive disk loss |
Database dumps are small and fast — usually 50 MB to a few GB compressed. Run them nightly. Media backups can be 10-100 GB for a busy site — run them weekly with incremental syncs in between.
The Practical Backup Setup for a Self-Hosted CMS
The setup most teams should run looks like this:
1. Nightly DB Dump via Cron
#!/bin/bash
# /usr/local/bin/backup-db.sh
DATE=$(date +%F)
BACKUP_DIR=/var/backups/db
mkdir -p "$BACKUP_DIR"
mysqldump -u backup_user -p"$DB_PASS" --single-transaction \
--routines --triggers your_cms_db \
| gzip > "$BACKUP_DIR/cms-$DATE.sql.gz"
# Keep 30 days locally
find "$BACKUP_DIR" -name "cms-*.sql.gz" -mtime +30 -delete
Cron entry:
30 2 * * * /usr/local/bin/backup-db.sh
Two important details: --single-transaction prevents table locks during the dump (your CMS keeps serving requests); --routines --triggers catches stored procedures and triggers most people forget about.
2. Weekly Media Sync via rsync
#!/bin/bash
# /usr/local/bin/backup-media.sh
rsync -a --delete \
/var/www/cms/storage/app/public/ \
/var/backups/media/
Weekly cron:
0 3 * * 0 /usr/local/bin/backup-media.sh
rsync is the right tool here because it only transfers changed files. A 50 GB media library that changed by 200 MB takes seconds to back up, not hours.
3. Offsite Copy via S3-Compatible Storage
This is the layer most teams skip and the one that saves you when the server itself dies. Tools that work well for self-hosted setups:
- restic — encrypted, deduplicated, supports S3, Backblaze B2, Wasabi, local. Free, open source.
- rclone — sync to any S3-compatible target. Lighter than restic, no deduplication.
- AWS CLI —
aws s3 syncif you're already on AWS.
Backblaze B2 currently runs $6/TB/month — cheaper than S3 for backup workloads. For a typical CMS install with 100 GB of media, that's $0.60/month. Stop having "we can't afford remote backups" conversations.
4. The Built-In Option (If You're on Laravel/UnfoldCMS)
If your CMS is Laravel-based and ships with Spatie Laravel Backup, you already have most of this. It packages DB dumps + storage folders into a single tarball and ships them to a configured disk (local, S3, B2, Dropbox). Schedule it from routes/console.php:
Schedule::command('backup:clean')->daily()->at('01:00');
Schedule::command('backup:run')->daily()->at('01:30');
UnfoldCMS bundles this. Admins can view, download, and trigger backups from /admin/system/backups. For a deeper look at what's shipped vs custom, see Self-Hosted CMS Security: A Practical Guide — backup is the operational sibling to security.
The Restore Drill (The Part Everyone Skips)
A backup you've never restored is a wish, not a backup. The single highest-leverage thing you can do for backup confidence is run a quarterly restore drill on a clean environment.
The Drill Steps
- Spin up a staging server — same OS, PHP version, MySQL version, web server as production. A $5/month DigitalOcean droplet is fine.
- Install the CMS from scratch — same git tag as production, run composer + migrations.
- Restore the most recent DB backup —
gunzip < cms-latest.sql.gz | mysql -u root staging_db. - Restore the media files —
rsync /var/backups/media/ staging:/var/www/cms/storage/app/public/. - Boot the site, log in, navigate. Check: do posts load? Do images load? Can you log in as an admin? Does the comment count match production?
- Take notes on every step that broke or required undocumented knowledge. That's where your real risk lives.
Time investment: 60-90 minutes the first time, 20-30 minutes thereafter. Run it the Saturday morning after you change anything in the backup pipeline.
What Usually Goes Wrong on the First Drill
Three things tend to surface during the first restore that nobody anticipated:
- Missing
.envrecipes. You restored the DB but forgot the encryption key, so all the encrypted settings are gibberish. Fix: back upAPP_KEYseparately, version-controlled in a secure secrets store. - File permissions wrong. rsync preserved permissions from the backup machine, not the production user (
www-dataon most stacks). Fix:chown -R www-data:www-data storage/. - Cache poisoning. The restored config cache references old paths. Fix:
php artisan config:clear && cache:clear && view:clearafter every restore.
Catch these once in a drill, never again in production.
Backup Encryption — When You Actually Need It
If your backup contains user PII (emails, names, password hashes), it needs to be encrypted in transit and at rest. This is non-negotiable for GDPR-regulated workloads and a strong default for everyone else.
In transit: use SSH, HTTPS, or S3 with server-side encryption. Don't FTP backups to a shared host. Don't sync to public-readable buckets.
At rest: encrypt before upload. restic does this by default. rclone supports it via --crypt. Old-school: gpg -c each tarball before transferring. Whichever path you pick, store the encryption key separately from the backups — a backup that's encrypted with a key stored in the same backup is uneconomical, not encrypted.
For more on GDPR-compliant data handling on self-hosted CMS, Self-Hosted CMS and GDPR: Data Sovereignty covers the compliance angle in depth.
Backup Retention — How Long to Keep What
A common mistake: keeping daily backups forever, running out of disk space, then disabling backups when the disk fills up. The fix is a tiered retention policy.
| Backup type | Keep how long | Where |
|---|---|---|
| Hourly snapshots (if you do them) | 24 hours | Local disk |
| Daily DB dumps | 30 days | Local disk + offsite |
| Weekly media snapshots | 12 weeks | Offsite |
| Monthly full snapshots | 12 months | Offsite, cold storage |
| Yearly archive | Indefinite | Cold storage |
For 100 GB of media, this works out to roughly: 100 GB × 1 weekly × 12 = 1.2 TB at the warm tier, ~$7/month on Backblaze B2. Add DB dumps and you're at $10/month total. That's the going rate for "I will never lose customer data."
restic handles tiered retention via --keep-daily 30 --keep-weekly 12 --keep-monthly 12 --keep-yearly 5. One config, done.
What to Back Up That's NOT Obvious
Three things teams typically forget that bite them at restore time:
1. Database user permissions. Your mysqldump saves data but not the GRANT statements. If you rebuild MySQL from scratch on the new server, you'll need to recreate the CMS DB user with the right permissions. Add --routines --triggers to your dump and keep a separate mysql --execute "SHOW GRANTS FOR cms_user@localhost" capture.
2. Cron jobs. If your CMS depends on * * * * * php artisan schedule:run (it does, for scheduled posts), that cron entry is NOT in your DB backup. Document it in your runbook or restore steps.
3. SSL certificates. If you use Let's Encrypt with certbot, the certs are in /etc/letsencrypt/. Back them up — re-issuing on a new server takes minutes but only if your DNS still resolves. If DNS is the problem, you're stuck without the existing certs.
A complete backup runbook lists all three of these alongside the technical commands. Don't trust your future self to remember them under pressure.
Soft CTA — Where This Fits in UnfoldCMS
UnfoldCMS ships Spatie Laravel Backup wired into the admin at /admin/system/backups. Admins can trigger backups, download them, and delete old ones from the UI. The disk target is configurable — local by default, S3/B2 with two settings changes. For agencies running multiple client installs, the same pattern works per-install — back up each client's CMS independently into the agency's shared object storage, organized by client subfolder.
The bigger story — why running your own backup process beats trusting a SaaS vendor's promises — is covered in Self-Hosted CMS for Agencies: Multiple Client Sites and Avoiding Vendor Lock-In With Self-Hosted CMS. Backups are the operational backbone of "your data, your control."
FAQ
How often should a self-hosted CMS be backed up?
Database: daily at minimum, on-deploy is better. Media: weekly. Both should sync offsite within 24 hours of the local backup. For high-traffic sites with frequent content updates, hourly DB snapshots make sense — most teams don't need them.
Is mysqldump good enough for production backups?
Yes, for most CMS workloads. Add --single-transaction to avoid locking tables and --routines --triggers to catch stored procedures. For very large databases (>50 GB), look at logical-vs-physical alternatives like Percona XtraBackup or MySQL Enterprise Backup, which are faster to restore. Below 50 GB, mysqldump is fine.
Should backups be encrypted?
If they contain user PII, password hashes, or API keys: yes, encrypt at rest and in transit. The cost is one config flag in restic/rclone and a separately-stored key. If your backup contains nothing sensitive (rare for a CMS), encryption is still a defense-in-depth recommendation.
How do you back up a CMS without taking it offline?
mysqldump --single-transaction keeps the database serving requests while you dump. rsync reads files without locking them. Done together they produce a consistent backup with zero user-visible downtime. The only thing that requires downtime is restoring — but on a separate staging server, that's a non-issue.
What's the cheapest reliable backup setup?
Local cron + Backblaze B2 via restic. About $1-10/month for 100 GB-1 TB of versioned, encrypted, deduplicated offsite storage. No vendor lock-in, no per-API-call fees, no surprise bills. The setup takes about an hour the first time.
Methodology
Setup steps and command examples above were tested on Ubuntu 24.04 with MySQL 8.0 and PHP 8.3. Pricing pulled from Backblaze B2 and AWS S3 public pricing pages as of May 2026. The 3-2-1 rule is industry-standard (referenced in NIST SP 800-34, ITIL service continuity guidance) — the specific application to self-hosted CMS is operational interpretation. The "restore drill" pattern is borrowed from financial-services DR practice; it works just as well for content systems and most teams under-invest in it.
If you have a backup-related setup that works at your scale and disagrees with anything above, write me — the operational practices here will keep improving with input from teams running real loads.
Share this post:
Leave a Comment
Please log in to leave a comment.
Don't have an account? Register here