Files
kbeandClaude Sonnet 5 dd4fd8cb9c fix: point dump_db.sh at its /opt/backup-agent deploy path, remove healthcheck
dump_db.sh no longer lives under $TARGET, so its own default dump
directory (relative to wherever the script is) would land outside the
backed-up tree. Pin DUMP_SCRIPT to /opt/backup-agent/dump_db.sh and
export DUMP_DIR explicitly so dumps still land in $TARGET/mariadb/dump
regardless of where the script itself is deployed.

Also drop the healthcheck integration (HEALTHCHECK_URL, send_healthcheck,
curl requirement) from borg-backup.sh per request - no monitoring hook
wanted for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:30:17 +02:00

203 lines
6.8 KiB
Markdown

# Backup & Recovery Runbook
Covers `borg-backup.sh` (daily backup), `dump_db.sh` (MariaDB logical
dumps, invoked by the backup script), and `restore.sh` (recovery).
## 1. One-Time Setup
Run once, by hand, on the server:
1. Initialize the encrypted Borg repo:
```bash
mkdir -p /home/srv/files/backups
borg init --encryption=repokey-blake2 /home/srv/files/backups/borg-2025
```
2. Create the passphrase file used by both backup and restore:
```bash
echo 'your-strong-passphrase' > /root/.borg-passphrase
chmod 600 /root/.borg-passphrase
```
3. Create the MariaDB `backup` user used by `dump_db.sh` (read-only, no
stop/lock of the server required thanks to `--single-transaction`):
```sql
CREATE USER 'backup'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT, LOCK TABLES, SHOW VIEW, TRIGGER, PROCESS, RELOAD ON *.* TO 'backup'@'%';
```
```bash
echo 'a-strong-password' > /root/.mariadb-backup.pw
chmod 600 /root/.mariadb-backup.pw
```
4. Create the root password file used only by `restore.sh` (restore needs
CREATE/DROP privileges the `backup` user does not have):
```bash
echo 'the-mariadb-root-password' > /root/.mariadb-root.pw
chmod 600 /root/.mariadb-root.pw
```
5. Exclude MariaDB's raw data directory from the archive. Find the
directory bind-mounted into the container as its datadir and drop a
marker file in it:
```bash
touch /home/srv/files/content/mariadb/data/.nobackup
```
This is what allows backups to run with the container up: only the
logical dump under `mariadb/dump/` is ever archived or restored from.
6. Configure the `scaleway` rclone remote:
```bash
rclone config
# create a remote named "scaleway", type S3, matching your Scaleway
# Object Storage credentials and region
```
## 2. Deploying the Scripts
Copy `borg-backup.sh`, `dump_db.sh`, and `restore.sh` to `/opt/backup-agent/`
(this exact path is what `borg-backup.sh` invokes for `dump_db.sh`). Make
all three executable:
```bash
chmod +x /opt/backup-agent/borg-backup.sh /opt/backup-agent/restore.sh
chmod +x /opt/backup-agent/dump_db.sh
```
## 3. Scheduling
Add a cron entry to run the backup daily, off-peak:
```
# /etc/cron.d/borg-backup
30 2 * * * root /opt/backup-agent/borg-backup.sh >> /var/log/borg/cron.log 2>&1
```
Check the last run:
```bash
ls -lt /var/log/borg/backup-*.log | head -1 # latest log file
tail -50 /var/log/borg/backup-*.log # inspect it
```
## 4. Day-2 Operations
List archives:
```bash
./restore.sh --list-archives
```
Check repo size and health:
```bash
BORG_PASSCOMMAND="cat /root/.borg-passphrase" borg info /home/srv/files/backups/borg-2025
```
Rotate the passphrase (creates a new key, re-encrypts nothing — old
archives still need the old passphrase to read, so keep both until fully
migrated):
```bash
BORG_PASSCOMMAND="cat /root/.borg-passphrase" borg key change-passphrase /home/srv/files/backups/borg-2025
```
Stale lockfile (backup or restore aborted mid-run and left the repo
locked):
```bash
BORG_PASSCOMMAND="cat /root/.borg-passphrase" borg break-lock /home/srv/files/backups/borg-2025
```
## 5. Recovery Procedures
All `restore.sh` commands accept `--dry-run` to preview exactly what would
happen without touching anything, and `--archive NAME` to target a
specific archive instead of the latest (see archive names via
`--list-archives`). Root DB credentials come from `MYSQL_ROOT_PASSWORD` in
the environment if set, otherwise from `/root/.mariadb-root.pw` — set
whichever is more convenient for how you're invoking it. Restore logs go
to `/var/log/borg/restore-*.log` (the `RESTORE_LOGDIR` environment
variable overrides the directory, mainly useful for testing). `full` and
`db` share `borg-backup.sh`'s lockfile, so a restore refuses to start
while the nightly backup is mid-run (and vice versa) rather than racing
it.
### 5.1 Full disaster recovery (new or wiped server)
Use when the whole server/container is gone and you're rebuilding from
scratch.
```bash
# 1. Reinstall borg, docker, and the mariadb container image/compose file
# (not covered by restore.sh - this is infra provisioning).
# 2. Restore the passphrase file (from your password manager / secondary
# backup - it is NOT stored in the repo it protects) to
# /root/.borg-passphrase, and the root DB password to
# /root/.mariadb-root.pw.
# 3. Preview:
./restore.sh full --dry-run
# 4. Run for real. --force is only required if /home/srv/files/content
# already has data in it (e.g. a stale mount); omit it on a genuinely
# empty/fresh server:
./restore.sh full --force
```
This extracts the full content tree from the archive, starts the
`mariadb` container and waits for it to report healthy, then restores
every database dump (users/grants first) — the container must be running
before any of the dump restores, which is why it starts first.
**Verify afterward:**
- `docker ps` shows `mariadb` running and healthy.
- The application responds normally.
- Spot-check row counts on a couple of tables against what you'd expect.
### 5.2 Single database restore
Use when one database got corrupted or someone ran a bad migration/query
against it — this **drops and recreates** that database.
```bash
./restore.sh db shopdb --dry-run # preview
./restore.sh db shopdb # prompts: type "shopdb" to confirm
```
Non-interactive (e.g. scripted from a monitoring alert): add `--yes` to
skip the typed confirmation.
**Verify afterward:** connect to the database and check the tables/row
counts you expect.
### 5.3 Single file/directory restore
Use for accidental deletion of a file, or to inspect an old version — this
never touches the running database or container.
```bash
./restore.sh file path/relative/to/content/some-file.txt --dest /tmp/recovered
```
The final location of the recovered item is printed at the end (it lands
under `/tmp/recovered/home/srv/files/content/...` — Borg preserves the
absolute path it was archived with).
## 6. Restore Drill Cadence
Quarterly, run a real `full` restore into a scratch directory (not
`/home/srv/files/content`) to confirm backups are actually usable:
`borg extract` always extracts into the current directory (there's no
`--destination` flag — this is why `restore.sh` itself `cd`s into the
destination before extracting), and the archive name must come from
`borg list --short` (plain `borg list` prints a formatted line, not a bare
name), so:
```bash
mkdir -p /tmp/restore-drill && cd /tmp/restore-drill
export BORG_REPO=/home/srv/files/backups/borg-2025
export BORG_PASSCOMMAND="cat /root/.borg-passphrase"
LATEST=$(borg list --short | tail -1)
borg extract --lock-wait 600 "::$LATEST"
```
Confirm the dump files under `mariadb/dump/` are present, non-empty, and
importable (`mysql -u root -p < mariadb/dump/somedb.sql` against a
throwaway MariaDB container). Log the drill date and outcome somewhere
durable (e.g. a team wiki page).