How it works in one sentence
rsync --link-dest=<previous-snapshot> <source> <new-snapshot> compares each source file against the same path in <previous-snapshot>; if identical (same content, size, mtime), rsync creates a hard link instead of copying. New / changed files copy fully. Result: <new-snapshot> looks like a complete tree, but disk usage is only the changed files.
The minimum-viable script
#!/bin/bash
# /usr/local/bin/snapshot-backup.sh
set -euo pipefail
SOURCE=/home/amir
DEST=/mnt/backup/snapshots
TIMESTAMP=$(date +%Y-%m-%d_%H%M%S)
# Most-recent existing snapshot becomes our --link-dest reference
LAST=$(ls -1tr "$DEST" 2>/dev/null | tail -n 1 || true)
LAST_OPT=""
if [ -n "$LAST" ]; then
LAST_OPT="--link-dest=$DEST/$LAST"
fi
# Make the new snapshot
NEW="$DEST/$TIMESTAMP"
mkdir -p "$NEW"
rsync -aHAX --numeric-ids --delete \
$LAST_OPT \
--exclude='.cache' --exclude='.local/share/Trash' --exclude='node_modules' \
"$SOURCE/" "$NEW/"
# Atomic "current" symlink
ln -sfn "$TIMESTAMP" "$DEST/current"
# Prune older than 30 days (keep all daily for 14d, then weekly for 30d, then monthly)
# Simplest version: keep last 30 snapshots, delete older
ls -1tr "$DEST" | grep -v '^current$' | head -n -30 | while read OLD; do
rm -rf "$DEST/$OLD"
done
Run from a systemd timer (see that tutorial) nightly. Each run takes the time to scan + copy only the changed files; subsequent days are typically minutes even for large home directories.
What the flags mean
-a— archive mode (recurses, preserves owner/group/permissions/timestamps/symlinks/special-files)-H— preserve hard links in the source itself-A— preserve ACLs-X— preserve extended attributes (important on Linux for SELinux, capabilities, file annotations)--numeric-ids— preserve UIDs/GIDs as numbers (don't try to map across systems; UID 1000 stays UID 1000)--delete— remove files in$NEWthat are no longer in$SOURCE--link-dest=<path>— hard-link unchanged files from this path instead of copying
What the snapshots look like on disk
$ ls /mnt/backup/snapshots/
2026-05-20_03:00:00
2026-05-21_03:00:00
2026-05-22_03:00:00
current -> 2026-05-22_03:00:00
$ du -sh /mnt/backup/snapshots/*
12G 2026-05-20_03:00:00 # apparent size
12G 2026-05-21_03:00:00 # apparent size (but mostly hard links)
12G 2026-05-22_03:00:00 # apparent size (but mostly hard links)
# Actual disk usage (de-duplicated by hard link)
$ du -sh /mnt/backup/snapshots/
12.3G /mnt/backup/snapshots/
Each snapshot directory looks like a 12 GB tree; the total disk usage across all snapshots is one full copy plus the daily deltas.
Restoring
The killer property: nothing special required. Each snapshot is a real directory tree.
# Restore one file
cp /mnt/backup/snapshots/2026-05-15_03:00:00/Documents/important.docx ~/Documents/
# Restore a directory
rsync -av /mnt/backup/snapshots/2026-05-15_03:00:00/Documents/ ~/Documents/
# Browse a snapshot
ls /mnt/backup/snapshots/2026-05-15_03:00:00/
# Compare yesterday vs today
diff -rq /mnt/backup/snapshots/{2026-05-21_03:00:00,2026-05-22_03:00:00}
No "restore wizard," no separate restore tool, no backup-tool-specific knowledge required. Years from now, when whatever backup software you used is gone, the snapshots are still readable by any Unix.
Across machines: rsync over SSH
# Pull-style: backup server pulls from each client
rsync -aHAX --numeric-ids --delete \
--link-dest=/mnt/backup/snapshots/amir-laptop/$LAST_LAPTOP \
amir@laptop:/home/amir/ \
/mnt/backup/snapshots/amir-laptop/$TIMESTAMP/
# Or push-style: client pushes to backup server
rsync -aHAX --numeric-ids --delete \
--link-dest=/mnt/backup/snapshots/$LAST \
/home/amir/ \
backup-server:/mnt/backup/snapshots/$TIMESTAMP/
Pull-style is generally cleaner: the backup server holds the credentials (one direction of trust); clients don't need any special config beyond an SSH key the server uses.
Retention with proper rotation
The "keep last 30" pattern is simple but loses long-term history. A more useful retention:
# Keep:
# - all of last 14 days
# - weekly snapshots for 8 weeks
# - monthly snapshots for 12 months
# Easiest approach: organize folders by day-of-week and week-of-month
DAILY_DIR="$DEST/daily"
WEEKLY_DIR="$DEST/weekly"
MONTHLY_DIR="$DEST/monthly"
# Daily snapshot
do_snapshot "$DAILY_DIR" 14 # rotate among 14 dated subfolders
# Weekly — on Sunday, copy daily/today to weekly
if [ "$(date +%w)" = "0" ]; then
cp -al "$DAILY_DIR/$TIMESTAMP" "$WEEKLY_DIR/$(date +%Y-W%U)"
# cp -al = archive + hard-link; doesn't double disk space
fi
# Monthly — on the 1st of the month
if [ "$(date +%d)" = "01" ]; then
cp -al "$DAILY_DIR/$TIMESTAMP" "$MONTHLY_DIR/$(date +%Y-%m)"
fi
Three tiers, hard-linked together, total disk usage still O(1 full copy + total deltas across all retention).
Limitations
- Same filesystem required for hard links. The destination must be one filesystem (you can't
--link-destacross mounts). For multi-disk setups, span them with LVM / mdadm (see that tutorial) or ZFS / Btrfs. - No encryption. The snapshots are plaintext on disk. For an untrusted backup target, layer LUKS underneath or use restic instead.
- No content-defined deduplication. Hard links match whole files; if a 1 GB file has 1 byte changed, the new snapshot stores the full 1 GB again. restic / borg dedup at chunk level and would store ~64 KB.
- Lots of small files = lots of inodes. Each snapshot still has a directory entry for every file. On filesystems with low inode limits, this matters; ext4 / xfs / zfs are fine in practice for normal home-dir use.
- No remote object store. rsync wants a Unix filesystem. For "back up to S3," use restic.
When --link-dest is the right tool
- Backups to a local USB drive or NAS, on the same trust domain as the source.
- You want browseable snapshots without learning a backup tool's CLI.
- You want backup data that's restorable with standard Unix commands forever.
- The data isn't sensitive enough to require client-side encryption.
For everything else, restic (see that tutorial) or Btrfs send/receive (see that tutorial) fits better. Often the right answer is both: rsync --link-dest to a local disk for "I deleted a file yesterday, restore it in 10 seconds" plus restic to S3 for "the house burns down, restore from elsewhere."