Skip to content

How to Automate Server Backups (and Actually Test Them)

This page may contain affiliate links.

Everyone has a backup plan until the day they need it. The uncomfortable truth is that an untested backup isn't a backup — it's a hope. I've watched a homelabber lose three years of photos because their rsync job had been silently failing for months after a disk UUID changed. The script ran, the cron job fired, the logs said "success", and the destination folder was empty. So this guide is about two things: automating backups properly, and proving they work. Both matter equally.

We'll cover a simple, robust approach you can run on any Linux box — a VPS, a Raspberry Pi, an old Optiplex in the shed — plus a restore drill that takes ten minutes and will save you a very bad weekend.

Start with the 3-2-1 rule, not with a tool

Before you touch a single script, decide what you're actually protecting. The classic framework is 3-2-1: three copies of your data, on two different types of media, with one copy off-site. For a home server that usually means:

  • Copy 1: the live data on the server itself.
  • Copy 2: a local backup to a USB drive or NAS on your own network.
  • Copy 3: an encrypted copy in cloud storage or at a friend's house.

For the local copy, any USB 3.0 external drive will do — a 4TB drive costs roughly the price of a decent takeaway for four people, and you can find a range of options here: external hard drives on Amazon UK. The point isn't the brand, it's that it's a separate device from your server. A second partition on the same disk is not a backup.

Write the script properly (and make it fail loudly)

Here's a minimal but genuinely solid backup script using rsync. Save it as /usr/local/bin/backup.sh and make it executable with chmod +x.

#!/bin/bash
set -euo pipefail

SRC="/srv/data/"
DEST="/mnt/backup/data/"
LOG="/var/log/backup.log"
STAMP=$(date +%Y-%m-%d_%H%M)

echo "[$STAMP] Starting backup" >> "$LOG"

# Snapshot-style copy: hardlinks unchanged files, saves space
rsync -aAXH --delete --link-dest="$DEST/current" \
  "$SRC" "$DEST/$STAMP/" >> "$LOG" 2>&1

# Update the 'current' symlink for next run's link-dest
ln -sfn "$DEST/$STAMP" "$DEST/current"

# Keep the last 14 snapshots
cd "$DEST" && ls -1dt */ | grep -v current | tail -n +15 | xargs -r rm -rf

echo "[$STAMP] Backup complete" >> "$LOG"

The set -euo pipefail line is the most important bit. Without it, a failed rsync can still exit zero and your cron job will happily report success. This makes the script stop the moment anything goes wrong.

The --link-dest trick gives you cheap incremental snapshots: unchanged files become hardlinks, so fourteen daily snapshots of a 500GB dataset might only use 520GB total. You get versioning without the storage cost.

Schedule it with cron — crontab -e and add:

0 3 * * * /usr/local/bin/backup.sh

Get the off-site copy right

A local backup protects you from accidental deletion and disk failure. It does not protect you from a burglary, a flood, or a very enthusiastic toddler. For the off-site leg, rclone is the tool to reach for — it speaks to Backblaze B2, S3, Google Drive, Dropbox and dozens of others with one syntax.

Install it, run rclone config to set up your remote, then add a second cron line:

30 4 * * * rclone sync /srv/data/ remote:server-backup --bwlimit 8M

The --bwlimit 8M caps upload bandwidth so your evening Netflix doesn't stutter. Always encrypt before upload — either use your provider's server-side encryption or, better, rclone's client-side crypt remote so the provider can't read your files.

If you're doing this seriously, an encrypted external SSD for the rotate-and-take-off-site copy is worth considering: encrypted external SSDs are small enough to live in a coat pocket.

Now the bit everyone skips: test the restore

Two-thirds of the way through, this is where most guides wave cheerfully and stop. Don't. A backup you have never restored is a rumour.

Here's a restore drill you can run monthly. First, verify the files exist and aren't zero-byte:

find /mnt/backup/data/current -type f -size 0 | head

An empty result is what you want. Now do a real restore into a scratch directory:

mkdir -p /tmp/restore-test
rsync -aAXH /mnt/backup/data/current/ /tmp/restore-test/
diff -r /srv/data /tmp/restore-test | head -50

If diff is quiet, your backup matches the source. If it isn't, you've just found the problem on a Tuesday afternoon instead of during a crisis.

For databases, a file copy isn't enough — you need a logical dump. For PostgreSQL:

pg_dump -U postgres mydb | gzip > /mnt/backup/db/mydb_$(date +%F).sql.gz

Then actually load it into a throwaway container and run a SELECT COUNT(*) on your biggest table. That's the test that matters.

Finally, wire up a dead man's switch. Healthchecks.io (free tier is plenty) gives you a URL to ping at the end of your script. If the ping doesn't arrive, it emails you. That single line catches silent failures that logs never will:

curl -fsS --retry 3 https://hc-ping.com/YOUR-UUID > /dev/null

Make the setup repeatable

Once you've got this working, resist the urge to hand-tune it forever. Put your scripts in a Git repo, document the restore procedure in a README, and store your credentials in a password manager rather than in the script itself. The whole point of automation is that future-you — the one woken at 2am by a failed disk — doesn't have to remember how any of it works.

If you'd rather not write all this from scratch, the Home Lab & Automation Pack is a set of ready-made AI prompts for scripting, troubleshooting and documenting exactly this kind of setup — useful if you want the done-for-you version of what we've just walked through, from £9.

Conclusion

Automated backups are one of those rare bits of home lab work that pay off enormously the one time you need them. Get the 3-2-1 structure right, write a script that fails loudly, push an encrypted copy off-site, and — this is the part that separates the prepared from the hopeful — restore something on purpose every month. Set a calendar reminder now. Your future self, staring at a dead drive, will thank you.

Written by

Richard Tucker

View all posts →