lastcopy

Docs

Every backup you watch has a unique ping URL, shown on its page (PING_URL below). Your job curls it when it finishes. Send numeric fields and Lastcopy learns what normal looks like, then alerts when a "successful" run doesn't look right.

Quick start

  1. Sign up. You land on your first backup check with its ping URL ready.
  2. Add one line to your backup job, like && curl -fsS PING_URL -d "bytes=$SIZE" || curl -fsS PING_URL/fail. Copy-paste recipes for restic, borgmatic, pg_dump, mysqldump, Veeam and rsync are below and on every check page.
  3. Let it learn. After 7 runs, baselines are active and a "successful" backup that doesn't look right gets flagged. Add a weekly integrity check and a quarterly restore drill when you're ready for the higher tiers.

The API

GET  PING_URL          # bare check-in (heartbeat only)
POST PING_URL          # check-in with metrics (form-encoded or JSON)
GET  PING_URL/start    # job started, enables duration and "started but never finished"
POST PING_URL/fail     # explicit failure, alerts immediately

The well-known fields are bytes, files, duration, snapshot, repo_size, snapshot_count, status (ok or fail) and exit. Any other numeric field is baselined too.

Recipes

restic

restic backup /srv --json | tail -n1 > /tmp/summary.json
curl -fsS -m 10 --retry 3 PING_URL -d "$(jq -r \
  '"bytes=\(.data_added)&files=\(.files_new + .files_changed)&duration=\(.total_duration|floor)&snapshot=\(.snapshot_id[0:8])"' \
  /tmp/summary.json)"

borgmatic

# /etc/borgmatic/config.yaml
commands:
  - after: action
    when: [create]
    run:
      - curl -fsS -m 10 --retry 3 PING_URL -d "status=ok"
  - after: error
    run:
      - curl -fsS -m 10 PING_URL/fail

pg_dump

pg_dump mydb > /backup/mydb.sql && gzip -f /backup/mydb.sql \
  && curl -fsS -m 10 --retry 3 PING_URL -d "bytes=$(stat -c%s /backup/mydb.sql.gz)&files=1" \
  || curl -fsS -m 10 PING_URL/fail
# on macOS use  stat -f%z  instead of  stat -c%s

Dump to a file, then compress. The pipe form (pg_dump | gzip > file) reports gzip's exit code in plain sh, so a failing dump still looks like success unless your shell has pipefail. Cron runs sh.

mysqldump

mysqldump --all-databases > /backup/all.sql && gzip -f /backup/all.sql \
  && curl -fsS -m 10 --retry 3 PING_URL -d "bytes=$(stat -c%s /backup/all.sql.gz)&files=1" \
  || curl -fsS -m 10 PING_URL/fail

Veeam (post-job PowerShell)

$s = Get-VBRBackupSession | Sort-Object CreationTime | Select-Object -Last 1
Invoke-RestMethod -Uri "PING_URL" -Method Post -Body @{
  status   = $s.Result
  bytes    = $s.Info.BackedUpSize
  duration = [int]($s.EndTime - $s.CreationTime).TotalSeconds
}

rsync

OUT=$(rsync -a --stats /srv/ backup:/srv/) \
  && curl -fsS -m 10 --retry 3 PING_URL -d "bytes=$(echo "$OUT" | awk '/Total transferred file size/ {gsub(/,/,""); print $5}')" \
  || curl -fsS -m 10 PING_URL/fail

The three assurance tiers

Lastcopy is honest about what each signal proves. Ran means the job checked in on schedule and its numbers look normal, a tripwire for silent failures that says nothing about restorability. Verified means your backup tool read the repository data back and checked it against its own checksums, like restic check --read-data or borg check --verify-data, and passed. Drilled means you actually restored the backup and validated the contents. Auditors (ISO 27001 A.8.13, SOC 2) accept documented restore tests as evidence, and the drill log is that register, assembled automatically.

Tier 2 · repository verification

For restic, check exits non-zero when errors are found. Plain restic check verifies structure only, and reading data is what detects corruption. --read-data-subset=n/t checks slice n of t, so if you rotate the slice weekly you cover the whole repository every t weeks. (The x% form picks a random subset, so repeats don't guarantee coverage.) Note that restic check takes an exclusive lock, so schedule it away from your backup window.

SLICE=$(( $(date +%V) % 5 + 1 ))   # rotate 1..5 by ISO week
restic check --read-data-subset=$SLICE/5 \
  && curl -fsS PING_URL/verify -d "verify=pass&subset=$SLICE/5" \
  || curl -fsS PING_URL/verify -d "verify=fail"

borg exits 0 for ok, 1 for a warning and 2 for fatal. --verify-data reads, decrypts and cryptographically verifies all data. It is slow, because the default check is CRC-only. Never automate --repair.

borg check --verify-data ::  ; RC=$?
if [ $RC -eq 0 ]; then curl -fsS PING_URL/verify -d "verify=pass&method=verify-data"
else curl -fsS PING_URL/verify -d "verify=fail&rc=$RC"; fi

For PostgreSQL base backups, pg_verifybackup checksums every file against the manifest and parses required WAL by default (use -n for tar-format backups). It exits non-zero on any failure.

pg_verifybackup /backup/base \
  && curl -fsS PING_URL/verify -d "verify=pass&method=pg_verifybackup" \
  || curl -fsS PING_URL/verify -d "verify=fail"

mysqldump has no integrity checker of its own. gzip -t validates the compression layer and the final -- Dump completed line catches truncation. Treat both as weak signals and rely on drills instead.

Tier 3 · restore drills

A drill is a restore into a scratch environment with content validation, run on your infrastructure. Lastcopy schedules it (set a drill cadence on the check), nags when it's overdue, and records the measured results as your audit register. Here is a PostgreSQL example.

docker run -d --name drill -e POSTGRES_PASSWORD=x postgres:16
until docker exec drill pg_isready -U postgres -q; do sleep 1; done
gunzip -c /backup/mydb.sql.gz | docker exec -i drill psql -U postgres -v ON_ERROR_STOP=1 -1
ROWS=$(docker exec drill psql -U postgres -tAc "SELECT count(*) FROM orders")
docker rm -f drill
[ "$ROWS" -gt 0 ] \
  && curl -fsS PING_URL/drill -d "result=pass&rows_restored=$ROWS&by=$USER" \
  || curl -fsS PING_URL/drill -d "result=fail&by=$USER"

A drill passes when the restore exits 0 and the validation queries match expectations (row counts, checksums). A failing drill alerts immediately, because that backup may not be recoverable.

The ping URL is the credential

Treat it like a password. It lives in your backup job and nowhere else. If it ends up somewhere it shouldn't (a ticket, a paste, a shared screen), rotate it from the backup's page and the old URL stops working immediately. Lastcopy never sends the URL in email and the site tells browsers to send no Referer header to any other site.

Notification policy

Designed against alert fatigue. Every email should be actionable, and roll-ups beat repeats.

How detection works

When Lastcopy itself is down

The register's no-edit rule includes us, so our own outages are recorded in it rather than smoothed over. Two detectors run all the time. The scheduler heartbeats every 15 seconds, and it also checks the public check-in path the way your job would. A scheduler gap longer than 5 minutes, or a check-in path that stays unreachable longer than that, is written into the register automatically as a service window.