#!/bin/bash # pre-backup.sh — ana-docker. # Runs as root from resticprofile's `run-before`, before `restic backup`. # # Produces DB dumps in /var/lib/restic/stage/ so the nightly restic # snapshot captures consistent point-in-time data for services whose # raw volume files are not safe to back up live. # # Containers handled here: # - synapse-db (internal Postgres 16 — pg_dump) # - seafile-mysql (internal MariaDB 10.6 — mysqldump) # - vaultwarden (external Postgres on PFI-Postgres 10.250.50.80) # - gitea (`gitea dump` captures DB + repos + config + LFS) # - openwebui (local SQLite × 2 — main db + ChromaDB vector store) # # External DB credentials live in /etc/restic/dbcreds.env (root:600). # Template: configs/restic/ana-docker/dbcreds.env.example in the repo. # # Required tooling on the host: # docker — always # pg_dump — for vaultwarden external Postgres; install via # `apt install postgresql-client`. Without it, # the vaultwarden block logs a warning and skips. # # Intentionally NOT handled: # - mattermost (retired 2026-04-21 — stack dir lingers but is not running) # # Idempotent: clears and recreates its staging files each run. # Errors in individual blocks are logged as WARN but don't abort the whole # script — partial dumps are better than no dumps. set -euo pipefail STAGE=/var/lib/restic/stage install -d -o root -g root -m 0700 "$STAGE" log() { printf '%s pre-backup(ana-docker): %s\n' "$(date -Is)" "$*"; } warn() { log "WARN: $*" >&2; } # Purge previous stage so stale dumps don't pile up into the snapshot. find "$STAGE" -mindepth 1 -maxdepth 1 -exec rm -rf {} + # Load external-DB creds. Silently skipped if missing — individual blocks # that need them will log their own WARN. if [ -r /etc/restic/dbcreds.env ]; then set -a; . /etc/restic/dbcreds.env; set +a fi # ---------- synapse (internal Postgres) --------------------------------------- if docker inspect synapse-db >/dev/null 2>&1; then log "dumping synapse postgres" docker exec synapse-db \ pg_dump -U synapse -d synapse -Fc --clean --if-exists \ > "$STAGE/synapse.pg_dump" \ || warn "synapse pg_dump failed" else log "skip synapse: container not present" fi # ---------- seafile (internal MariaDB) ---------------------------------------- if docker inspect seafile-mysql >/dev/null 2>&1; then log "dumping seafile mariadb" docker exec seafile-mysql sh -c \ 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases --single-transaction --quick 2>/dev/null' \ | gzip -c > "$STAGE/seafile.sql.gz" \ || warn "seafile mysqldump failed" else log "skip seafile: container not present" fi # ---------- vaultwarden (external Postgres on PFI-Postgres 10.250.50.80) ------ # The vault moved from SQLite to external Postgres (date unclear). Any # /data/db.sqlite3* files in the container are stale leftovers and should # be deleted separately — this hook captures the live Postgres data only. if docker inspect vaultwarden >/dev/null 2>&1; then if [ -z "${VW_PGPASS:-}" ]; then warn "vaultwarden: VW_PGPASS unset in /etc/restic/dbcreds.env — skipping" elif ! command -v pg_dump >/dev/null 2>&1; then warn "vaultwarden: pg_dump not installed — skipping (apt install postgresql-client)" else log "dumping vaultwarden postgres (external: ${VW_PGHOST}:${VW_PGPORT:-5432})" PGPASSWORD="$VW_PGPASS" pg_dump \ -h "$VW_PGHOST" -p "${VW_PGPORT:-5432}" \ -U "$VW_PGUSER" -d "$VW_PGDB" \ -Fc --clean --if-exists \ > "$STAGE/vaultwarden.pg_dump" \ || warn "vaultwarden pg_dump failed" fi else log "skip vaultwarden: container not present" fi # ---------- gitea (native `gitea dump`) --------------------------------------- # `gitea dump` produces a single archive with the DB dump, repo trees, # config, LFS objects and attachments. The in-container command reads # its own DB creds (from GITEA__database__* env vars) — no external # creds needed. # # `--type tar` produces an UNCOMPRESSED tarball. Compressed formats # (zip/tar.gz) defeat restic's content-defined chunking: each day's # dump looks completely different to restic even when the underlying # data barely changed, so repo grows by ~full dump size every day. # Uncompressed tar lets restic dedup aggressively — after the first # snapshot, daily incrementals only cost the actual new-data delta. # # Tradeoff: on-disk stage file is larger (~2-3x the zip size) but that's # transient (deleted next run). Repo-side storage is much smaller. # # Size trimming flags if the tar becomes excessive: --skip-lfs-data, # --skip-repository, --skip-attachment-data. if docker inspect gitea >/dev/null 2>&1; then log "dumping gitea (gitea dump, uncompressed tar)" if docker exec -u git gitea sh -c \ 'rm -f /tmp/gitea-dump.tar && gitea dump -c /data/gitea/conf/app.ini -f /tmp/gitea-dump.tar --type tar' \ >/dev/null 2>&1; then docker cp gitea:/tmp/gitea-dump.tar "$STAGE/gitea-dump.tar" \ && docker exec -u git gitea rm -f /tmp/gitea-dump.tar \ || warn "gitea dump copy/cleanup failed" else warn "gitea dump command failed" fi else log "skip gitea: container not present" fi # ---------- openwebui (local SQLite × 2) -------------------------------------- # Two SQLite databases: main app (/app/backend/data/webui.db) and the # ChromaDB vector store (.../vector_db/chroma.sqlite3). Uses SQLite's # .backup command for a consistent snapshot if sqlite3 is available in # the container; falls back to restic's volume-level capture otherwise. OWUI_CONTAINER=openwebui-open-webui-1 if docker inspect "$OWUI_CONTAINER" >/dev/null 2>&1; then if docker exec "$OWUI_CONTAINER" sh -c 'command -v sqlite3 >/dev/null 2>&1'; then log "dumping openwebui sqlite (webui.db + chroma.sqlite3) via .backup" for pair in \ "/app/backend/data/webui.db:webui.db" \ "/app/backend/data/vector_db/chroma.sqlite3:chroma.sqlite3"; do src=${pair%:*}; dst=${pair#*:} if docker exec "$OWUI_CONTAINER" sqlite3 "$src" ".backup /tmp/$dst" 2>/dev/null; then docker cp "$OWUI_CONTAINER:/tmp/$dst" "$STAGE/openwebui.$dst" \ && docker exec "$OWUI_CONTAINER" rm -f "/tmp/$dst" \ || warn "openwebui copy/cleanup failed for $dst" else warn "openwebui .backup failed for $src (missing file or locked?)" fi done else warn "openwebui: sqlite3 not in container — relying on restic volume-level capture" fi else log "skip openwebui: 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"