The Backup Strategy That Actually Protects Your VPS Data (And Why Automation Isn't Optional)

Manual backups fail under pressure, and unmonitored automated ones fail silently. A layered VPS backup strategy, the 3-2-1 rule, application-aware dumps, encrypted off-site transfer, dead-man's-switch monitoring.
The Backup Strategy That Actually Protects Your VPS Data (And Why Automation Isn't Optional)
"I have backups" is one of the most common last words in infrastructure. Usually what that means is a manual tar command someone ran once, six months ago, sitting on the same disk it's supposed to be protecting. That's not a backup — it's a false sense of security with a timestamp on it.
Here's the strategy I actually run across the VPS infrastructure behind my production stacks — MongoDB replica sets, multitenant CRM backends, and the self-hosted services sitting on top of them — built around automation, verification, and the assumption that every backup will eventually be tested by an actual disaster, not just a cron log that says "success."
Tip
TL;DR — Follow the 3-2-1 rule: 3 copies of your data, on 2 different storage types, with 1 copy off-site. Automate the whole pipeline with cron and a tool like rsync, BorgBackup, or Restic — never a manually-run script. Encrypt before it leaves the host, verify with a monitoring ping so a silent failure actually alerts you, and restore-test on a schedule. A backup you haven't restored is a hypothesis, not a guarantee.
Why "I have a cron job" isn't a strategy by itself
| Manual backups | Automated, unmonitored backups | Automated + monitored + tested backups | |
|---|---|---|---|
| Consistency | Depends on someone remembering | Consistent until the job silently fails | Consistent, and failures are caught |
| Failure visibility | None until you need it | None — a broken cron job looks identical to a working one from the outside | Immediate — a missed ping triggers an alert |
| Restore confidence | Unknown | Unknown | Verified on a recurring schedule |
| Effort after setup | High, ongoing | Low, until it fails | Low, plus a small recurring restore-test task |
| Real-world outcome | Fails under pressure or during busy periods | Fails silently, discovered during the actual incident | Fails loudly, discovered during a routine check |
The gap between the second and third columns is where almost every real data-loss story lives. A cron job that quietly stopped running three weeks ago because a disk filled up, a path changed, or a credential expired looks exactly like a healthy one — until the day you actually need it.
Layer 1: know what you're actually protecting
Before automating anything, separate what's on the VPS into the categories that actually matter, because a single "backup everything" snapshot is expensive, slow to restore, and often backs up things you don't need while missing things you do:
- Databases — MongoDB, PostgreSQL, MySQL. These need application-aware dumps (
mongodump,pg_dump), not a raw file copy, or you risk backing up a database mid-write and restoring something inconsistent. - Application data and user uploads — anything not reproducible from your Git repository: media, generated files, user-submitted content.
- Configuration — nginx configs, environment files, systemd units, cron definitions, SSL certificates. Small in size, disproportionately painful to lose, since rebuilding a server without them means re-deriving configuration from memory.
- System-level snapshot — a full VM/disk image, useful as a fast rollback point after a bad deployment or failed update, but not a substitute for application-aware backups of the layers above it.
Treating these as one undifferentiated blob is the most common reason backup strategies fail quietly — the full-disk snapshot ran fine, but the database was mid-transaction when it was taken.
Layer 2: automate the pipeline, don't run it by hand
Cron handles the vast majority of VPS backup automation well — the goal isn't a fancier scheduler, it's a pipeline that runs unattended, fails safely, and doesn't depend on anyone being awake.
#!/bin/bash
# /opt/backup/run-backup.sh — cron target, not a manual script
set -euo pipefail
BACKUP_DIR="/backups/$(date +%F)"
mkdir -p "$BACKUP_DIR"
# Application-aware database dump, not a raw file copy
mongodump --host "rs0/mongo1:27017,mongo2:27017" \
--readPreference secondary --gzip \
--archive="$BACKUP_DIR/mongo.gz"
# Application data and config, incremental via rsync
rsync -a --delete /var/www/app/uploads/ "$BACKUP_DIR/uploads/"
tar -czf "$BACKUP_DIR/config.tar.gz" /etc/nginx /etc/systemd/system /opt/app/.env
# Encrypt before it leaves the host
tar -czf - "$BACKUP_DIR" | gpg --symmetric --cipher-algo AES256 \
--output "$BACKUP_DIR.tar.gz.gpg"
rm -rf "$BACKUP_DIR"# /etc/cron.d/vps-backup — runs unattended, no human in the loop
0 3 * * * root /opt/backup/run-backup.sh >> /var/log/backup.log 2>&1set -euo pipefail at the top matters more than it looks — without it, a failed mongodump doesn't stop the script, and you can end up with a "successful" backup archive that's silently missing the most important piece.
For heavier or higher-frequency needs, BorgBackup or Restic add deduplication and incremental snapshots on top of this same pattern, which matters once daily full dumps start costing meaningful storage and bandwidth.
Layer 3: get it off the host, encrypted, before anything else happens
A backup that never leaves the VPS it protects isn't disaster recovery — it's a note for whoever inherits the postmortem after that VPS's disk fails, gets compromised, or gets deleted by mistake. The encrypted archive from Layer 2 needs to move somewhere else entirely:
# Push the encrypted archive to off-site object storage
aws s3 cp "$BACKUP_DIR.tar.gz.gpg" \
s3://your-backup-bucket/vps/ --storage-class STANDARD_IACloudflare R2 works identically here and avoids S3 egress fees if you ever need to pull backups back down for a real restore. Two things matter more than which provider you pick: the backup leaves the host it was taken on, and it's encrypted before it does — an unencrypted backup sitting in a bucket is just your production database with extra steps for whoever finds it.
Layer 4: monitor for silence, not just for errors
The failure mode that actually causes data loss isn't a backup script that crashes loudly — it's one that stops running at all and nobody notices for weeks. A cron job that was quietly disabled, a disk that filled up so the script exits before it starts, a credential that expired — all of these produce no output, which is exactly why "check the log if something looks wrong" doesn't catch them.
The fix is a dead-man's-switch style monitor: the backup script pings a monitoring endpoint on success, and if that ping doesn't arrive within the expected window, the monitor — not you — raises the alarm.
# Appended to the end of run-backup.sh, after a successful upload
curl -fsS -m 10 --retry 3 "https://your-monitor.example.com/ping/backup-job" > /dev/nullA self-hosted uptime monitor or a hosted status-check service both work for this — what matters is that a missing ping is treated as a failure, not just an error ping.
Layer 5: the part everyone skips — testing the restore
A backup archive that uploads successfully tells you the export worked. It tells you nothing about whether the restore will actually succeed against a clean environment, whether encryption keys are still valid and accessible, or whether the archive was silently truncated by a disk that filled up mid-run. I've seen more backup strategies fail at restore time than at backup time.
Run an actual restore, on a schedule, into a disposable environment that isn't production:
gpg --decrypt "$BACKUP_DIR.tar.gz.gpg" | tar -xzf - -C /tmp/restore-test
mongorestore --gzip --archive=/tmp/restore-test/mongo.gz --drop --db test_restore
mongosh --eval "db.getSiblingDB('test_restore').stats()"Compare document counts and spot-check a few records against what you expect. If this restore isn't automated and running at least monthly, the backup strategy is unverified — it's a folder of .gpg files nobody's opened, not a disaster recovery plan.
A simple decision model
Cron + rsync/mongodump is enough if:
- Data volume is small to moderate
- Daily backups satisfy your acceptable data-loss window (RPO)
Move to BorgBackup or Restic if:
- Storage and bandwidth costs from daily full backups are adding up
- You need faster incremental backups with deduplication
Either way, non-negotiable:
- 3-2-1: three copies, two storage types, one off-site
- Encrypted before it leaves the host
- A monitored ping so a silent failure gets caught, not discovered
- A restore actually run and verified on a recurring scheduleWrapping up
Automation is what makes a backup strategy survive contact with real life — busy weeks, on-call fatigue, and the fact that nobody reliably remembers to run a manual script forever. But automation alone only solves the "did it run" problem. Monitoring solves "did it keep running," and restore-testing solves the one question that actually matters: "if I needed this today, would it work?" A strategy missing any of the three isn't incomplete — it's untested, which in backups amounts to the same thing.
This is the same discipline behind the MongoDB replica set backup strategy I run in production, and it sits on the same self-hosted infrastructure covered in building my own Firebase alternative.
Related Posts
Related Articles

Rate Limiting and Caching at the API Gateway Layer: Protecting Your Backend From Its Own Traffic
Most outages aren't attacks, they're your own traffic overwhelming itself. A concept-first look at how rate limiting and caching work together at the API gateway to keep one misbehaving client, integration, or traffic spike from degrading service for everyone else.

Backup Strategy for a Dockerized MongoDB Replica Set: Disaster Recovery You've Actually Tested
A replica set isn't a backup — it protects against a dead node, not a dropped collection or a bad migration. Here's the mongodump/oplog setup I actually run against a Dockerized MongoDB replica set, why restore testing is the step everyone skips, and when to graduate to filesystem snapshots or PBM i

Redis Pub/Sub vs Caching vs Rate Limiting: When to Add Each Layer
Redis can cache reads, enforce shared limits, or broadcast events—but each solves a different problem. Learn when to add each layer and when not to.
