Existing borg-backup.sh/dump_db.sh, old/ reference scripts, and the brainstormed design spec for restore tooling + runbook.
7.7 KiB
Backup & Recovery System Design
Date: 2026-07-25
Goal
Reliable, automated backup of /home/srv/files/content (including MariaDB
running in Docker) to a local Borg repo + offsite Scaleway S3 mirror, plus a
documented, scriptable recovery path so downtime is short and predictable.
Current State
old/borg.sh,old/maria.sh— scripts currently live on the server.borg-backup.sh,dump_db.sh(repo root) — improved replacements, not yet deployed. Add: encrypted repo (repokey-blake2viaBORG_PASSCOMMAND), flock-based locking, preflight checks, container health polling, atomic dump staging/swap, non-transactional table warnings, integrity checks (borg check, daily archive-only / weekly--verify-data), retry/timeout wrapping, log rotation.- No restore tooling and no scheduling exist yet.
dump_db.sh is kept as-is. borg-backup.sh had one functional change made
during this design: it no longer stops the MariaDB container. dump_db.sh
already produces a transactionally-consistent dump via
--single-transaction, so a live logical dump is safe without stopping
anything (this only covers InnoDB tables — the script already warns if it
finds non-InnoDB tables). The remaining risk was borg also archiving
MariaDB's raw on-disk data directory while it's being written to; that's
resolved by excluding it from the archive entirely (drop a .nobackup
marker file in it — borg-backup.sh already passes
--exclude-if-present .nobackup), so only the logical dump ever gets
backed up, and restore only ever needs the dump anyway. Net effect: daily
backups run with zero MariaDB downtime. This exclusion step needs to be
added to the one-time setup, documented in the runbook.
Architecture
cron (daily) → borg-backup.sh
├─ dump_db.sh (mysqldump per-DB, atomic staging swap;
│ container stays up throughout)
├─ borg create (encrypted, zstd) → local repo
│ (raw MariaDB data dir excluded via .nobackup;
│ only the logical dump is archived)
├─ borg prune + compact
├─ borg check (daily fast / weekly full --verify-data)
└─ rclone sync → Scaleway S3 (offsite mirror)
restore.sh (manual, run on demand during an incident)
mode=full : fresh/broken server → extract full content dir from archive
→ restore all DBs → start container
mode=db : single database → restore one .sql from a chosen archive
mode=file : single file/dir → borg extract path, no DB involved
Components
All at repo root, deployed to the server alongside the existing scripts.
borg-backup.sh (existing, one change: no longer stops MariaDB)
Orchestrates the daily backup: dump → archive → prune → compact → check → offsite sync. The container is never stopped — the dump step alone produces a consistent backup, and the raw data directory is excluded from the archive. Already has locking, timeouts, health-check pings, and cleanup-on-exit logic (cleanup now only intervenes if the container happens to be down for an unrelated reason, restarting it defensively).
dump_db.sh (existing, unchanged)
Runs inside borg-backup.sh step 1. Dumps each non-system database plus
users/grants to a staging dir, verifies each dump file, then atomically
swaps staging into place. Deployed at
/home/srv/files/content/mariadb/dump_db.sh.
restore.sh (new)
restore.sh full [--archive NAME] [--force]
restore.sh db <db_name> [--archive NAME]
restore.sh file <path-within-target> [--archive NAME] [--dest DIR]
restore.sh --dry-run <above args>
restore.sh --list-archives
- Defaults
--archiveto the most recent archive in the repo. full: refuses to run if$TARGETis non-empty unless--forceis passed (protects against overwriting a working system by accident). Extracts the full archive over$TARGET, then restores every.sqldump found undermariadb/dump/using root DB credentials, then starts themariadbcontainer and waits for it to become healthy.db <name>: extracts onlymariadb/dump/<name>.sqlfrom the archive, prompts for the database name to be typed again as confirmation (it's destructive — drops/recreates the DB), restores it with root credentials.file <path>:borg extractof a single path from the archive into--dest(default: a scratch dir under/tmp), no DB or container involvement — safe, non-destructive.--dry-run: prints exactly what would run (archive chosen, paths extracted, commands) without touching anything.- Same logging conventions as
borg-backup.sh(/var/log/borg/restore-*.log), non-zero exit on any failure, no partial/silent restores — a failed step aborts before touching the "good" copy where possible. - Uses the same
BORG_PASSCOMMAND/ passphrase file as backups. - Restore DB operations authenticate as
rootviaMYSQL_ROOT_PASSWORDor a root password file (mirrors howdump_db.shresolves credentials), since restore needs full privileges (CREATE/DROP) that the limitedbackupuser used for dumping does not have.
RUNBOOK.md (new)
Single operational document covering:
- One-time setup (run once, by hand, not scripted):
borg init --encryption=repokey-blake2, create/root/.borg-passphrase(chmod 600), create thebackupMariaDB user with the grantsdump_db.shneeds (SELECT, LOCK TABLES, SHOW VIEW, TRIGGER, PROCESS, RELOAD) and its password file/root/.mariadb-backup.pw, drop a.nobackupmarker file into MariaDB's Docker volume data directory (excludes raw DB files from the archive — only the logical dump undermariadb/dump/gets backed up), configurerclonefor thescalewayremote, set the real healthcheck URL. - Deploying the scripts: where each file goes, permissions,
chmod +x. - Scheduling: cron entry (daily, off-peak hours) invoking
borg-backup.sh, plus how to check the last run (/var/log/borg/, healthcheck dashboard). - Day-2 operations: listing archives, checking repo size/health, rotating the passphrase, what to do if the lockfile is stale.
- Recovery procedures — one clearly-numbered walkthrough per
scenario (full disaster, single DB, single file), written so someone
unfamiliar with the internals can follow it step-by-step under
pressure, including exact
restore.shinvocations and what to verify afterward (container healthy, app responds, row counts sane). - Restore drill cadence: recommend a quarterly test restore into a scratch location to confirm backups are actually usable.
Error Handling & Safety
restore.shnever overwrites live data without an explicit--force(full) or typed confirmation (db).- All destructive steps are logged before execution.
--dry-runavailable for every mode.- Failures abort immediately (
set -euo pipefail), matching the existing scripts' style; partial state is called out explicitly in the log.
Testing
No automated test framework — these are ops scripts against real infrastructure. Verification consists of:
restore.sh --dry-runruns against the real repo to sanity-check archive selection and command construction.- Quarterly real restore drill into a scratch directory (documented in the runbook), confirming dumps are complete and importable.
Out of Scope
- No changes to
dump_db.sh, and no changes toborg-backup.shbeyond removing the DB stop/start (see Components above). - No monitoring/alerting beyond the existing healthcheck ping.
- No multi-server / multi-target generalization — this is specific to
/home/srv/files/content.