#!/bin/bash # pre-backup.sh — esh-docker-vm. # Runs as root from resticprofile's `run-before`. # # Produces consistent DB dumps in /var/lib/restic/stage/ for services # whose raw volume files risk inconsistency during live restic capture. # # Unique approach for this host: most containers don't bundle sqlite3, # so we run sqlite3 from the HOST against the volume bind-mount paths. # Requires sqlite3 installed on esh-docker-vm (apt install sqlite3). # # Services handled: # - home-assistant (local SQLite in volume — sqlite3 .backup from host) # - calibre-web-automated (local SQLite — sqlite3 .backup inside container, has sqlite3) # - pgadmin (local SQLite in volume — sqlite3 .backup from host) # # uptime-kuma was here until it moved to ana-docker (2026-09-22). Its block # was removed 2026-09-23: with no container left to match, its unguarded # `docker ps | grep` lookup exited 1 and set -e aborted this script, and # resticprofile then skipped the WHOLE host backup (stale 09-22 → 09-23). # # paperless-ngx's Postgres (on esh-vm-db, 10.0.50.60) was also dumped from # here until 2026-09-23, when the block was removed by operator decision. It # is backed up at the source by esh-vm-db's own fail-closed pg_dumpall, and # this copy had failed auth every night since 2026-04-24 behind a WARN # nobody read, leaving a 0-byte paperless.pg_dump in every snapshot. That # block was the only consumer of /etc/restic/dbcreds.env, so the creds # loader went with it. # # Errors in individual blocks log a WARN; whole script doesn't abort. # That only holds if every lookup for an optional service sits inside an # `if` test or ends in `|| true` — under set -euo pipefail a bare # `x=$(… | grep …)` that matches nothing kills the script, and a failed # run-before hook means NO snapshot at all, not a partial one. set -euo pipefail STAGE=/var/lib/restic/stage install -d -o root -g root -m 0700 "$STAGE" log() { printf '%s pre-backup(esh-docker-vm): %s\n' "$(date -Is)" "$*"; } warn() { log "WARN: $*" >&2; } # Purge previous stage so stale dumps don't pile up in the snapshot. find "$STAGE" -mindepth 1 -maxdepth 1 -exec rm -rf {} + # Helper: host-side sqlite .backup against a volume-bind path. # $1 = source .db path (host absolute, typically under /var/lib/docker/volumes/.../_data/) # $2 = stage filename (just the leaf name) host_sqlite_backup() { local src="$1" dst="$STAGE/$2" if ! command -v sqlite3 >/dev/null 2>&1; then warn "sqlite3 not on host (apt install sqlite3) — skipping $2" return 1 fi if [ ! -f "$src" ]; then warn "source db missing: $src — skipping $2" return 1 fi if sqlite3 "$src" ".backup '$dst'" 2>/dev/null; then log "dumped $2 ($(du -h "$dst" 2>/dev/null | cut -f1))" else warn "sqlite3 .backup failed for $src" rm -f "$dst" return 1 fi } # ---------- home-assistant (SQLite in named volume, host-side .backup) -------- # HA's DB is ~50MB and actively written. SQLite .backup is the proper way # to grab a consistent snapshot while HA is running. if docker inspect homeassistant >/dev/null 2>&1; then HA_DB="/var/lib/docker/volumes/homeassistant_homeassistant_data/_data/home-assistant_v2.db" host_sqlite_backup "$HA_DB" "home-assistant.sqlite3" || true else log "skip home-assistant: container not present" fi # ---------- calibre-web-automated (SQLite, sqlite3 inside container) --------- if docker inspect calibre-web-automated >/dev/null 2>&1; then log "dumping CWA sqlite via in-container .backup" if docker exec calibre-web-automated sqlite3 /config/app.db ".backup /tmp/cwa-app.db" 2>/dev/null; then docker cp calibre-web-automated:/tmp/cwa-app.db "$STAGE/calibre-web-automated.app.db" \ && docker exec calibre-web-automated rm -f /tmp/cwa-app.db \ || warn "CWA copy/cleanup failed" else warn "CWA sqlite .backup failed" fi else log "skip calibre-web-automated: container not present" fi # ---------- pgadmin (SQLite in named volume, host-side .backup) -------------- if docker inspect pgadmin4_container >/dev/null 2>&1; then PG_DB="/var/lib/docker/volumes/pgadmin_pgadmin-data/_data/pgadmin4.db" host_sqlite_backup "$PG_DB" "pgadmin4.db" || true else log "skip pgadmin: container not present" fi # ---------- summary ----------------------------------------------------------- size=$(du -sh "$STAGE" 2>/dev/null | awk '{print $1}') count=$(find "$STAGE" -type f | wc -l) log "stage ready: $count files, $size total"