1e24e0f799
Closes the last file-level backup gap. Primary ESH home-lab Docker host
— five services with state worth consistent dumps:
- paperless-ngx → external Postgres on 10.0.50.60 (host pg_dump)
- home-assistant → local SQLite ~50MB (host sqlite3 .backup)
- calibre-web-automated → local SQLite (in-container sqlite3)
- pgadmin → local SQLite (host sqlite3)
- uptime-kuma → local SQLite (host sqlite3; container name may vary
after force-recreate, detect by label)
Unique to this host: HA/pgadmin/uptime-kuma images don't bundle sqlite3.
Rather than maintaining custom images, pre-backup.sh runs sqlite3 from
the HOST against the volume bind-mount paths. Requires sqlite3 +
postgresql-client installed on esh-docker-vm.
Cross-site writes to rest-server-ana since ESH has no local rest-server.
NFS mounts (/mnt/{backup,books,compose,documents}) explicitly excluded
— hundreds of GB of NAS-side content backed up at the NAS layer. Also
excludes offen-sidecar buffer volumes (paperless + pgadmin currently
run offen/docker-volume-backup alongside — retire once restic has a
week of clean runs).
Found in audit (non-blocking but noted in README follow-ups):
- paperless-ngx Postgres password is literally "paperless-ng" —
trivially weak, rotate at next opportunity.
130 lines
5.3 KiB
Bash
130 lines
5.3 KiB
Bash
#!/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 and pg_dump from the HOST against the volume
|
|
# bind-mount paths. Requires sqlite3 + postgresql-client installed
|
|
# on esh-docker-vm (apt install sqlite3 postgresql-client).
|
|
#
|
|
# Services handled:
|
|
# - paperless-ngx (external Postgres on 10.0.50.60 — pg_dump from host)
|
|
# - 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 (local SQLite in volume — sqlite3 .backup from host)
|
|
#
|
|
# External DB credentials live in /etc/restic/dbcreds.env (root:600).
|
|
# Template: configs/restic/esh-docker-vm/dbcreds.env.example.
|
|
#
|
|
# Errors in individual blocks log a WARN; whole script doesn't abort.
|
|
|
|
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 {} +
|
|
|
|
# Load external-DB creds
|
|
if [ -r /etc/restic/dbcreds.env ]; then
|
|
set -a; . /etc/restic/dbcreds.env; set +a
|
|
fi
|
|
|
|
# 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
|
|
}
|
|
|
|
# ---------- paperless-ngx (external Postgres on 10.0.50.60) -------------------
|
|
if docker inspect paperless-ngx-webserver-1 >/dev/null 2>&1; then
|
|
if [ -z "${PAPERLESS_PGPASS:-}" ]; then
|
|
warn "paperless-ngx: PAPERLESS_PGPASS unset in /etc/restic/dbcreds.env — skipping"
|
|
elif ! command -v pg_dump >/dev/null 2>&1; then
|
|
warn "paperless-ngx: pg_dump not installed — apt install postgresql-client"
|
|
else
|
|
log "dumping paperless postgres (${PAPERLESS_PGHOST}:${PAPERLESS_PGPORT:-5432})"
|
|
PGPASSWORD="$PAPERLESS_PGPASS" pg_dump \
|
|
-h "$PAPERLESS_PGHOST" -p "${PAPERLESS_PGPORT:-5432}" \
|
|
-U "$PAPERLESS_PGUSER" -d "$PAPERLESS_PGDB" \
|
|
-Fc --clean --if-exists \
|
|
> "$STAGE/paperless.pg_dump" \
|
|
|| warn "paperless pg_dump failed"
|
|
fi
|
|
else
|
|
log "skip paperless: container not present"
|
|
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
|
|
|
|
# ---------- uptime-kuma (SQLite in named volume, host-side .backup) ----------
|
|
# Container name may vary after force-recreates (e.g. <hash>_uptime-kuma).
|
|
# Detect by label rather than hardcoded name.
|
|
UK_CONTAINER=$(docker ps --filter "label=com.docker.compose.project=uptimekuma" --format "{{.Names}}" | head -1)
|
|
[ -z "$UK_CONTAINER" ] && UK_CONTAINER=$(docker ps --format "{{.Names}}" | grep -E "uptime.kuma" | head -1)
|
|
if [ -n "$UK_CONTAINER" ]; then
|
|
UK_DB="/var/lib/docker/volumes/uptimekuma_uptime-kuma/_data/kuma.db"
|
|
host_sqlite_backup "$UK_DB" "uptime-kuma.kuma.db" || true
|
|
else
|
|
log "skip uptime-kuma: no container matching"
|
|
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"
|