Guide

Backups that run themselves — and tell you when they don't.

The rsync command is the easy part. Making it run every night for five years, never overlap itself, and complain loudly when it fails — that's the actual engineering.

cron or systemd timers?

Both are fine; the honest differences: cron is universal and one line, but has no built-in overlap protection, no missed-run catch-up, and logging is whatever you redirect. systemd timers give you Persistent=true (a machine that was off at 2 AM runs the backup at boot), automatic journal logging, and unit-level status you can query. For a laptop or a machine with irregular uptime, timers win clearly. For an always-on server, cron plus a lock is perfectly respectable.

# cron with flock — the one-liner that prevents overlapping runs
15 2 * * * root flock -n /run/lock/offsite.lock /usr/local/bin/offsite-backup.sh >> /var/log/offsite-backup.log 2>&1

flock -n exits immediately if last night's run is somehow still going — the failure mode that otherwise stacks rsync processes on a slow week until the box crawls.

The systemd version

# /etc/systemd/system/offsite-backup.service
[Unit]
Description=Offsite rsync backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/offsite-backup.sh
Nice=10
IOSchedulingClass=idle

# /etc/systemd/system/offsite-backup.timer
[Unit]
Description=Nightly offsite backup

[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=15m

[Install]
WantedBy=timers.target
systemctl enable --now offsite-backup.timer
systemctl list-timers offsite-backup.timer   # next run, last run
journalctl -u offsite-backup.service -n 50   # the log

Bonus details: Type=oneshot means overlap protection is built in (a still-running service won't start again), IOSchedulingClass=idle keeps the backup from starving production I/O, and RandomizedDelaySec stops a fleet from hammering the destination at the identical second.

Alerting: the part everyone skips

A backup system's most dangerous state is silently broken. Minimum viable alerting is a script that exits non-zero on rsync failure and something that notices. Two low-dependency patterns:

# in the backup script — mail on failure (needs a working MTA or msmtp)
if ! rsync -a --delete ... ; then
  echo "offsite backup FAILED on $(hostname) at $(date)" \
    | mail -s "BACKUP FAILURE: $(hostname)" you@example.com
  exit 1
fi

# or: dead-man's-switch — ping a heartbeat URL only on success
rsync -a --delete ... && curl -fsS https://your-heartbeat-service/ping/TOKEN

The heartbeat pattern is stronger: it catches every failure mode including "the server died," because the alert fires when the ping stops arriving, not when an error is sent. For systemd, OnFailure= can trigger a notification unit as well.

Being polite to your own network

If backups share an uplink with production, cap them: --bwlimit=20M holds rsync to ~20 MB/s. Uploads to RSYNCIT are unmetered, so the limit exists purely to protect your own daytime traffic — many people run uncapped at night and --bwlimit only on daytime catch-up runs. Combine with -z on slow links, skip it on fast ones where CPU becomes the bottleneck.

A destination that's always up for it

Point your timer at flat-rate storage with unmetered uploads. $6.95/TB per month, provisioned same-day.

See pricing

TL;DR

  • cron + flock, or systemd oneshot + timer
  • Persistent=true for machines that sleep
  • Alert on failure — heartbeat beats email
  • --bwlimit protects your daytime uplink
  • Test restores monthly, not annually