An rsync Backup Script With Snapshots, a Mount Guard and a Restore Drill

An rsync Backup Script With Snapshots, a Mount Guard and a Restore Drill

A folder called Backup on an external drive, copied by hand sometime last year, holding roughly a third of what actually matters. That is what most people have when they say they have a backup, and they discover it the day the laptop refuses to boot. So the framing is wrong: most people do not have a backup problem, they have a verification problem. Copying files is easy and nearly everyone eventually does it. Knowing that a copy is current, complete, and genuinely restorable is a different discipline, and it takes maybe an hour to set up properly. Four years of design work or a decade of family photos is not protected by a folder with a hopeful name. What follows is an example script built from the rsync documentation, plus the routine around it; run it with --dry-run against a test folder before trusting it with real data.

a backup you have never restored is a rumour

Start with the uncomfortable test. If your main drive died in the next ten minutes, what exactly would you do, in what order, and how long would it take? If you cannot answer that from memory, you don’t yet have a backup system. You have copies, which is a start, and I’d rather you had copies than nothing.

The failures here are rarely dramatic. A drive that was full for six months, so every run after that quietly did nothing. A sync that mirrored a deletion, wiping the good copy the day after the mistake. Permissions that weren’t preserved, so a restored project wouldn’t open. An encrypted archive whose passphrase lived only in the head of someone who’d since forgotten it. None of those look like failure on the day they happen. They look like a backup finishing.

three-two-one, adapted for an Indian flat

Four local adjustments to the 3-2-1 backup rule: protect against power cuts, avoid heat and humidity, seed off-site copies by hand, and use a relative's house as off-site.
Three copies, two kinds of storage, one off-site.

The old guidance is three copies of anything you care about, on two different kinds of storage, with one copy off-site. It holds up well, but the local realities change the details:

  • Power. A backup write interrupted by a supply cut is how filesystems get corrupted. A machine on an inverter, or a laptop with a working battery, is doing real protective work here. I explain that setup in the old laptop home server guide.
  • Heat and humidity. A portable drive left on a sunny windowsill through a Patna summer, or sitting through monsoon damp, will not last as long as the spec sheet implies. Store the off-site copy somewhere dry and shaded, not in a car.
  • Upload bandwidth. Home broadband here is heavily asymmetric. A first full upload of a large photo library can take days, so seed the off-site copy from a drive you physically carry, then let the schedule handle only the changes.
  • Off-site can mean your parents’ house. It doesn’t have to be a paid cloud. A drive that lives at a relative’s place across town, swapped when you visit, satisfies the requirement completely.

the rsync flags that actually matter

Table of rsync flags: -a archive mode, -H hard links, --delete, --exclude-from, and --dry-run with --itemize-changes.
Run every new or edited command with –dry-run first.
Comparison: with a trailing slash rsync copies the contents of /home/anurag; without it, it creates a nested anurag folder inside the target.
A dry run makes this visible before it costs a whole backup.

rsync has an intimidating manual and about six flags you’ll use regularly. Here is the shape of a first working command, and then what each part is protecting you from:

rsync -aH --delete \
  --exclude-from=/home/anurag/bin/backup-excludes.txt \
  --dry-run --itemize-changes \
  /home/anurag/ /mnt/backup/anurag/

-a is archive mode, which preserves permissions, ownership, timestamps and symlinks. Skip it and you get files back without the metadata that made them usable. -H preserves hard links, which matters for the snapshot trick below. --delete makes the target mirror the source, so files you removed don’t linger forever, and it’s also the flag that can hurt you, which is why snapshots come next.

--dry-run paired with --itemize-changes is the habit I’d most like you to steal. It shows exactly what would change without changing anything. Run every new or edited backup command this way first, every time, and you will catch the trailing-slash mistakes that otherwise write files into the wrong directory level.

Speaking of which: in rsync, /home/anurag/ and /home/anurag mean different things. With the trailing slash you copy the contents into the target. Without it you copy the folder itself, creating /mnt/backup/anurag/anurag/. Nothing warns you.

an exclude file saves space and grief

Caches and build artefacts are large, change constantly, and are worthless in a restore. Keep the list in a file so it’s version-controlled and readable:

# backup-excludes.txt
.cache/
.local/share/Trash/
node_modules/
*.tmp
Downloads/
.thumbnails/
snap/

snapshots that let you go back in time

Flow: yesterday's snapshot, rsync with --link-dest hard-links unchanged files, today's snapshot stores only changes, and the latest symlink moves forward.
Every folder looks like a full copy, but disk use grows only by what changed.
Bash snapshot script with set -euo pipefail and a mountpoint guard highlighted, then rsync -aH --delete with --link-dest to a dated folder and a latest symlink.
Hard links only work when every snapshot lives on the same filesystem.

A plain mirror answers “what do my files look like now”. It cannot answer “what did this file look like before somebody broke it last week”. That second question is the one you’ll actually have, especially with the failure mode where you notice a corruption or an accidental overwrite days later.

--link-dest solves this beautifully. Each run writes a new dated folder, but any file that hasn’t changed is stored as a hard link to the previous run instead of a second copy. You get what look like full daily snapshots while paying disk space only for what changed.

#!/usr/bin/env bash
set -euo pipefail

# Refuse to run unless the backup drive is really mounted
mountpoint -q /mnt/backup || { echo "backup drive not mounted" >&2; exit 1; }

SRC=/home/anurag/
DEST=/mnt/backup/snapshots
TODAY=$(date +%F)
LATEST="$DEST/latest"

mkdir -p "$DEST"

rsync -aH --delete \
  --exclude-from=/home/anurag/bin/backup-excludes.txt \
  --link-dest="$LATEST" \
  "$SRC" "$DEST/$TODAY/"

# Point "latest" at the run we just completed
ln -sfn "$DEST/$TODAY" "$LATEST"

# Keep 30 days of snapshots, judged by each folder's date name, not its mtime
# (rsync -a copies the source folder's modification time onto every snapshot).
CUTOFF=$(date -d '30 days ago' +%F)
for dir in "$DEST"/20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]; do
  [ -d "$dir" ] || continue
  if [[ "$(basename "$dir")" < "$CUTOFF" ]]; then
    rm -rf -- "$dir"
  fi
done

Two notes. set -euo pipefail on line two makes the script stop at the first error rather than plough on and report success, which is how silent partial backups happen. And the hard links only work when every snapshot, including the one --link-dest points at, sits on the same filesystem, so keep $DEST on one drive rather than spanning mounts; the source can live anywhere.

running it unattended

Comparison: without a mount check an unmounted backup drive makes rsync fill the system disk; with mountpoint -q the script exits with an error instead.
One line turns a quiet disaster into an alert.

Once the script is right, hand it to a scheduler rather than your memory. I’d reach for a systemd timer with catch-up enabled, so a run missed during a power cut happens after the next boot, and that choice is argued out in detail in scheduling Linux jobs without silent failures. Whichever you pick, keep the mount guard at the top of the script, and make sure the scheduler refuses to start a second run while one is still going.

# Bail out early rather than writing to an unmounted mount point
mountpoint -q /mnt/backup || { echo "backup drive not mounted"; exit 1; }

That one line prevents a nasty outcome. Without it, if the drive fails to mount, /mnt/backup is just an empty folder on your root disk, and the script cheerfully fills your system drive with a copy of your own data.

the off-site copy and encryption

For the copy that leaves your house, add encryption. A drive you carry across town can be lost, and a cloud bucket is someone else’s computer. Two approaches I’d recommend depending on temperament: rclone with its crypt backend if you want your existing folder structure pushed to object storage, or restic if you’d rather have a proper deduplicating, encrypted repository with built-in integrity checking. restic’s check command verifying repository integrity is a real advantage over anything I can build with rsync alone.

Whatever you choose, write the passphrase down on paper and put it somewhere a family member can find it. A perfectly encrypted archive that nobody can open is indistinguishable from no backup, and this is the step people skip because it feels unserious compared to the scripting.

the quarterly restore drill

Five-step restore drill: pick a recent file and an old folder, restore to /tmp/restore-test, compare with sha256sum, check snapshot dates, and check free space.
A backup you have never restored is a rumour.

Put a recurring reminder in your calendar and do this four times a year. It takes ten minutes, and it is the part that turns a folder full of hopeful copies into something you can rely on at 2 AM:

  1. Pick a file you changed recently and a folder you haven’t touched in months.
  2. Restore both from a snapshot into /tmp/restore-test/ rather than over the originals.
  3. Compare them: sha256sum the restored file against the live one, and actually open the restored document.
  4. Check the dates on your snapshot folders. If the newest is three weeks old, the schedule is broken and you’ve just found out cheaply.
  5. Confirm free space on the target. A full drive is a common cause of a backup that stopped working months ago.

common mistakes

Five backup mistakes: a second partition on the same disk, --delete without snapshots, copying live database files, ignoring the phone, and losing the encryption passphrase.
Write the off-site passphrase on paper where a family member can find it.
  • Backing up to a second partition on the same physical disk. That protects against your own mistakes, not against the drive dying, which is the likelier event.
  • Using --delete with no snapshots. One bad sync then erases the only good copy.
  • Never checking the log. A backup script that emails or messages you only on failure is worth building; see the failure-alert pattern in the scheduling guide above.
  • Forgetting the databases. A running MySQL or Postgres data directory copied file-by-file may restore inconsistently. Dump it to a file first, then back up the dump.
  • Skipping the phone. Most people’s irreplaceable photos are on a phone, not a laptop. Point something at that too.
  • Leaving the backup drive permanently mounted and writable by everything. If you’re already hardening the box along the lines of my Ubuntu security checklist, treat an always-attached backup target as part of that review.

FAQ

Is rsync still the right tool, or should I use restic?

Both, for different jobs. rsync with --link-dest gives you snapshots you can browse with cd and ls, which is wonderfully easy to trust because nothing is hidden inside a format. restic gives you encryption, deduplication and integrity verification, which is what I want for the copy that leaves the house. Local snapshots with rsync, encrypted off-site with restic, is the combination I’d recommend.

How much space do the snapshots need?

Roughly one full copy of your data, plus whatever changes daily. Because unchanged files are hard links rather than duplicates, thirty daily snapshots of a mostly-static photo library cost very little more than one. Large files that change in place every day, like a VM disk image or a database file, are the exception and will consume a fresh copy each run, so exclude those and back them up a different way.

Can I back up to another machine over the network?

Yes, and rsync over SSH is the standard way. Use key-based authentication, restrict that key to the backup command on the receiving side, and run the transfer over your LAN for the initial seed. If your home lab already has a second always-on box, as in my home lab setup guide, that machine is a better target than a drive you have to remember to plug in.

What about ransomware reaching my backup drive?

This is the strongest argument for a copy that is not permanently connected. A drive you attach for the run and detach afterwards, or an off-site repository where the backup account cannot delete old data, both break the chain. Snapshots on an always-mounted drive help against accidents, but they are not proof against something with write access to that drive.

Update history: replaced the retention step, which could delete every snapshot including the new one, moved the mount guard into the script itself, and corrected the hard-link note.

Sources