#!/usr/bin/env bash # # Nightly backup for the key register. # # SQLite's .backup makes a consistent copy while the app is running — do NOT # just cp the .db file, because the WAL may hold recent writes that a plain # copy would miss. # # Install: # chmod +x backup.sh # sudo crontab -e # 20 3 * * * /opt/keyreg/backup.sh >> /var/log/keyreg-backup.log 2>&1 set -Eeuo pipefail APP_DIR="/opt/keyreg" BACKUP_DIR="/mnt/backup/keyreg" # MUST be a different physical disk OFFSITE_DEST="" # e.g. "backup@nas2:/volume1/keyreg" KEEP_DAYS=60 STAMP="$(date +%Y-%m-%d_%H%M)" OUT="${BACKUP_DIR}/keyreg-${STAMP}.db" log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*"; } trap 'log "FAILED at line $LINENO — backup NOT complete"; exit 1' ERR mkdir -p "$BACKUP_DIR" cd "$APP_DIR" # 1. Consistent snapshot log "Backing up database..." docker compose exec -T keyreg \ sqlite3 /data/keyreg.db ".backup '/data/_tmp_backup.db'" docker compose cp keyreg:/data/_tmp_backup.db "$OUT" docker compose exec -T keyreg rm -f /data/_tmp_backup.db # 2. Verify — an unreadable backup is not a backup sqlite3 "$OUT" "PRAGMA integrity_check;" | grep -q '^ok$' \ || { log "Integrity check FAILED"; exit 1; } ITEMS=$(sqlite3 "$OUT" "SELECT COUNT(*) FROM items;") [ "$ITEMS" -gt 0 ] || { log "Backup has zero keys — refusing to call this good"; exit 1; } gzip -9 "$OUT" log "OK — ${ITEMS} keys, $(stat -c %s "${OUT}.gz") bytes" # 3. A readable copy too. If the app is ever gone, a CSV still tells you # who holds which key — which is the whole point of the register. curl -fsS "http://127.0.0.1:3000/api/admin/export.csv" \ -o "${BACKUP_DIR}/keyreg-${STAMP}.csv" || log "WARNING: CSV export failed" # 4. Off site — survives fire and theft, which is when you most need to know # who has keys to the building if [ -n "$OFFSITE_DEST" ]; then rsync -az --delete "${BACKUP_DIR}/" "${OFFSITE_DEST}/" log "Off-site sync done" else log "WARNING: OFFSITE_DEST empty — local copy only" fi # 5. Retention find "$BACKUP_DIR" -maxdepth 1 -type f -mtime "+${KEEP_DAYS}" -delete log "Complete" # =========================================================================== # RESTORE — test this once now, not on the day you need it # # cd /opt/keyreg # docker compose down # gunzip -c /mnt/backup/keyreg/keyreg-DATE.db.gz > ./data/keyreg.db # rm -f ./data/keyreg.db-wal ./data/keyreg.db-shm # docker compose up -d # # That is the whole restore. One file. # ===========================================================================