The first time you write a backup script, it’s six lines: restic backup /home. The first time it silently stops running, you discover that the only signal something’s wrong is “I went to restore something and the most recent snapshot is from three months ago.” That’s how almost every cautionary backup tale starts. The fix isn’t a better script — it’s a script that tells someone, every day, whether it ran and whether it succeeded.
Healthchecks.io is the canonical answer for this. Free for personal use (and SOURCE-AVAILABLE if you’d rather self-host). Your script pings a URL on success; if HC.io doesn’t see a ping in the expected window, it emails / Slacks / SMSes you. Combine that with restic + B2 and you have a real backup system: encrypted, deduplicated, dead-man’s-switched.
Step 1 — create a Healthchecks.io check
Sign up at healthchecks.io (free tier covers 20 checks). Create a check named “backup-server-A.” Set:
- Schedule: simple, period 1 day, grace 6 hours. (Backup runs daily; if it doesn’t ping in 24h+6h, it alarms.)
- Notification methods: email + your phone. SMS is nice if you can spare $5/month. Slack if you live in Slack.
Copy the check’s ping URL. It looks like https://hc-ping.com/<uuid>. There’s also a /start variant (signal “starting now”) and a /fail variant (signal “this run failed”). We’ll use all three.
Step 2 — the restic environment file
# /root/restic.env (mode 600)
export RESTIC_REPOSITORY="b2:my-backup-bucket-2026:server-a"
export B2_ACCOUNT_ID="0012345..." # B2 application key ID
export B2_ACCOUNT_KEY="K001..." # B2 application key
export RESTIC_PASSWORD_FILE="/root/.restic-passphrase" # mode 600
# Healthchecks ping URL
export HC_URL="https://hc-ping.com/your-check-uuid-here"The passphrase file is the actual encryption key — lose it and you cannot decrypt the backup, full stop. Keep a copy in 1Password or written down in a fire-safe.
Step 3 — the backup script
#!/usr/bin/env bash
# /usr/local/sbin/backup-restic
# Usage: cron, daily at 03:30
set -uo pipefail
. /root/restic.env
LOG=/var/log/restic-backup.log
TS=$(date -Iseconds)
log() { echo "[$TS] $*" | tee -a "$LOG" ; }
trap 'rc=$?; if [ $rc -ne 0 ]; then
log "FAILED rc=$rc"
curl -fsS --retry 3 --max-time 10 \
--data-binary @- "${HC_URL}/fail" <<< "$(tail -50 $LOG)" >/dev/null
fi' EXIT
# Tell HC that we're starting (lets it track duration)
curl -fsS --retry 3 --max-time 10 "${HC_URL}/start" >/dev/null
log "begin backup"
restic backup \
/home /etc /var/lib/docker/volumes \
--exclude-caches \
--exclude='**/.cache/**' \
--exclude='**/node_modules/**' \
--exclude='**/.venv/**' \
--tag daily \
--quiet 2>&1 | tee -a "$LOG"
log "begin prune"
restic forget --tag daily \
--keep-daily 7 --keep-weekly 4 --keep-monthly 12 \
--prune --quiet 2>&1 | tee -a "$LOG"
log "begin check (small)"
restic check --read-data-subset=5% 2>&1 | tee -a "$LOG"
log "done"
# Final ping with last 50 log lines as the body (HC.io stores it for ~30 days)
curl -fsS --retry 3 --max-time 10 \
--data-binary @- "${HC_URL}" <<< "$(tail -50 $LOG)" >/dev/nullWhat this script gives you:
- A start ping, so HC.io can show “backup is currently running” on the dashboard.
- A success ping when everything completes — backup, prune, integrity check.
- A failure ping if any step exits non-zero, with the tail of the log attached so you can debug from your phone email.
- A 5% sampling integrity check on every run.
restic check --read-data-subset=5%picks random data blobs and verifies they decrypt — catches B2 silent corruption / your bitrot before you need a restore. - The
trapensures the FAIL ping fires even if the script crashes mid-way (e.g. signal, OOM kill).
Step 4 — cron it
# /etc/crontab — root's crontab
30 3 * * * root /usr/local/sbin/backup-resticDon’t pick 03:00 or 04:00 — thousands of other cron jobs land there. 03:30 (or 03:23 / 03:47) spreads load.
Step 5 — verify the alarm actually fires
Untested alerts are not alerts. Test the alarm path right now, before you trust it:
# Manually mark the check as failed:
curl -fsS "$HC_URL/fail"
# Within ~10 seconds you should get an email/Slack/SMS.
# If you don't: fix your HC.io notification settings before going to bed.Then trigger a real success ping by running the script:
sudo /usr/local/sbin/backup-resticWatch /var/log/restic-backup.log grow. The HC.io dashboard should flip to “up” within seconds of the run finishing.
A monthly chore: actually restore something
The most underrated part of a backup system is the “does the restore work” check. Once a month, on a calendar reminder, do this:
. /root/restic.env
mkdir -p /tmp/restore-test
restic restore latest --target /tmp/restore-test --include /home/me/Documents
diff -r /home/me/Documents /tmp/restore-test/home/me/Documents
rm -rf /tmp/restore-testIf diff is empty (modulo files modified since the snapshot), the backup chain works end-to-end. If diff complains, fix it now — the worst time to discover backup-rot is when you actually need it.
Cost
- Healthchecks.io: free for 20 checks. Hobbyist tier ($5/mo) for SMS notifications and longer log retention.
- Backblaze B2: $6/TB/month, $0.01/GB egress.
- ~200 GB source data: dedups to ~110 GB on B2 = ~$0.66/month.
Total: ~$1/month for backups that ping you the moment they break. Compare to a SaaS backup product where you pay 50x and still don’t know the alarm works until the day it doesn’t.
Photo: HTML code on a dark screen by Pixabay on Pexels.
