From c8cfae4192a35b91c442327a24b2458bd758e95e Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Mon, 10 Aug 2026 14:55:57 -0700 Subject: [PATCH] Add healthchecks.io dead-man's-switch to paperless backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We just found the daily backup LaunchAgent was never actually loaded on this machine, so the job silently didn't run for 62 days with no error to notice — a plain "notify on script failure" wouldn't have caught this since the script never even started. backup.sh now pings a healthchecks.io check on success, and its /fail endpoint via an ERR trap if any step fails. If the ping doesn't arrive on schedule (crashed run, unloaded agent, machine off), healthchecks.io raises the alert externally instead of relying on something local to notice its own absence. The ping URL is optional (read from Keychain) so backup.sh still runs fine without it configured. Co-Authored-By: Claude Sonnet 5 --- ansible/tasks/paperless_backup.yml | 10 ++++++++++ paperless-ngx/backup.sh | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ansible/tasks/paperless_backup.yml b/ansible/tasks/paperless_backup.yml index 9e6c581..54f1dbb 100644 --- a/ansible/tasks/paperless_backup.yml +++ b/ansible/tasks/paperless_backup.yml @@ -29,6 +29,16 @@ # # IMPORTANT: Also save the restic password in 1Password — lose it and backups are unrecoverable. # +# Optional but recommended: a healthchecks.io dead-man's-switch so a silently +# broken or unscheduled backup job (e.g. the LaunchAgent never getting loaded) +# gets flagged, not just script errors. Create a free check at +# https://healthchecks.io with a 24h period matching the daily 3am schedule +# (a few hours of grace time), then store its ping URL: +# +# security add-generic-password -a $USER -s paperless-backup-healthcheck-url -w 'https://hc-ping.com/' +# +# If this isn't set, backup.sh just skips the ping and runs as before. +# # Then initialise the restic repo once: # set -x B2_ACCOUNT_ID (security find-generic-password -a $USER -s paperless-backup-b2-id -w) # set -x B2_ACCOUNT_KEY (security find-generic-password -a $USER -s paperless-backup-b2-key -w) diff --git a/paperless-ngx/backup.sh b/paperless-ngx/backup.sh index ba96907..2f1d051 100755 --- a/paperless-ngx/backup.sh +++ b/paperless-ngx/backup.sh @@ -11,6 +11,19 @@ log() { echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "$LOG_FILE" } +HEALTHCHECK_PING_URL=$(security find-generic-password -a "$USER" -s paperless-backup-healthcheck-url -w 2>/dev/null || true) + +ping_healthcheck() { + [ -z "$HEALTHCHECK_PING_URL" ] && return 0 + curl -fsS -m 10 --retry 3 -o /dev/null "${HEALTHCHECK_PING_URL}${1:-}" || true +} + +on_error() { + log "Paperless backup failed" + ping_healthcheck /fail +} +trap on_error ERR + log "Starting paperless backup" export B2_ACCOUNT_ID @@ -34,3 +47,4 @@ log "Pruning old snapshots" restic -r b2:mb-paperless-backup:paperless forget --keep-daily 7 --keep-weekly 2 --keep-monthly 2 --prune log "Paperless backup complete" +ping_healthcheck