#!/usr/bin/env bash # # backup.sh — SQLite-safe backup of a running PocketBase pb_data directory. # # PocketBase's data.db and auxiliary.db are SQLite databases opened in WAL # mode. Copying the .db files directly with `cp` while the server is running # can grab an inconsistent snapshot if a checkpoint happens mid-copy. Using # sqlite3's `.backup` command (or PocketBase's own "backups" API) takes a # consistent snapshot safely without stopping the server. # # This script uses `sqlite3 .backup`, which is the simplest option that needs # no PocketBase superuser credentials. It backs up pb_data (including # uploaded files) into timestamped tarballs and prunes old ones. # # Usage: # ./backup.sh [PB_DATA_DIR] [BACKUP_DIR] # PB_DATA_DIR defaults to ../pb_data (relative to this script) # BACKUP_DIR defaults to ./backups (relative to this script) # # Suggested cron (nightly at 3am, keep last 14): # 0 3 * * * /opt/bookshelf/deploy/backup.sh >> /var/log/bookshelf-backup.log 2>&1 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PB_DATA_DIR="${1:-$SCRIPT_DIR/../pb_data}" BACKUP_DIR="${2:-$SCRIPT_DIR/backups}" KEEP_LAST="${KEEP_LAST:-14}" log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"; } fail() { log "ERROR: $*"; exit 1; } command -v sqlite3 >/dev/null 2>&1 || fail "sqlite3 is required (apt install sqlite3 / apk add sqlite)." [[ -d "$PB_DATA_DIR" ]] || fail "pb_data directory not found: $PB_DATA_DIR" TIMESTAMP="$(date -u '+%Y%m%d_%H%M%S')" WORKDIR="$(mktemp -d)" trap 'rm -rf "$WORKDIR"' EXIT mkdir -p "$BACKUP_DIR" log "Backing up pb_data from $PB_DATA_DIR" # Consistent SQLite snapshots via the .backup command (safe for a live, # WAL-mode database — it's the same mechanism `sqlite3 db.sqlite .backup` # uses, which briefly locks for the copy but never corrupts). for db in data.db auxiliary.db; do src="$PB_DATA_DIR/$db" if [[ -f "$src" ]]; then log " snapshotting $db" sqlite3 "$src" ".backup '$WORKDIR/$db'" fi done # Everything else under pb_data that isn't a sqlite file/WAL/SHM (i.e. # uploaded covers under storage/) gets copied as-is — these are immutable # blobs once written, so a plain copy is safe. log " copying uploaded files (storage/)" if [[ -d "$PB_DATA_DIR/storage" ]]; then cp -a "$PB_DATA_DIR/storage" "$WORKDIR/storage" fi ARCHIVE="$BACKUP_DIR/bookshelf-backup-$TIMESTAMP.tar.gz" tar -czf "$ARCHIVE" -C "$WORKDIR" . log "Wrote $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))" # Prune old backups, keep the most recent $KEEP_LAST. mapfile -t old < <(ls -1t "$BACKUP_DIR"/bookshelf-backup-*.tar.gz 2>/dev/null | tail -n +$((KEEP_LAST + 1))) if [[ ${#old[@]} -gt 0 ]]; then log "Pruning ${#old[@]} backup(s) older than the last $KEEP_LAST" rm -f "${old[@]}" fi log "Done." # --- Restore --------------------------------------------------------------- # 1. Stop the server: sudo systemctl stop bookshelf # 2. Move aside the live data: mv /opt/bookshelf/pb_data /opt/bookshelf/pb_data.bak # 3. Extract the chosen backup: mkdir /opt/bookshelf/pb_data && \ # tar -xzf bookshelf-backup-TIMESTAMP.tar.gz -C /opt/bookshelf/pb_data # 4. Fix ownership: sudo chown -R bookshelf:bookshelf /opt/bookshelf/pb_data # 5. Start the server again: sudo systemctl start bookshelf # 6. Once you've confirmed it's healthy, delete pb_data.bak.