Files
vh 57730444b6 restic/ana-docker: gitea dump — switch zip → uncompressed tar for dedup
The compressed zip defeated restic's content-defined chunking: each
day's dump looked completely different to restic even when the repo
content barely changed, causing repo growth of ~full dump size (821 MB
at last measurement) every day until forget/prune aged snapshots out.

Uncompressed tar is dedup-friendly. After the first snapshot, daily
incrementals cost only the actual new-data delta — typically a few
MB for an active repo.

Tradeoff: stage file on the client host is ~2-3x the zip size while
the dump is in flight, but that's transient (purged at the start of
each run). Repo-side storage is much smaller over time.
2026-04-21 01:14:03 -07:00

158 lines
6.7 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"