18 Commits
Author SHA1 Message Date
kbeandClaude Sonnet 5 bfafb09ee6 fix: also exclude logs/ (plural) directories from the archive
Some sites use logs/ instead of log/ (e.g. webscroll.fr/logs/nginx/) -
the previous re:/log/ pattern didn't match the plural, so those files
kept showing up as modified every backup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 23:06:32 +02:00
kbeandClaude Sonnet 5 cd17727ff9 fix: make rclone sync more resilient to Scaleway throttling
Offsite sync hit 17 fatal IO errors deleting stale segment files after a
large one-off prune batch (from the recent log/-exclude change), and
rclone's default behavior is to bail immediately on delete errors rather
than retry. Lower concurrency (--transfers 8->4, --checkers 16->8) to
reduce rate-limit pressure, and make retries explicit (--retries 5,
--retries-sleep 10s, --low-level-retries 20) instead of relying on
defaults.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 22:56:50 +02:00
kbeandClaude Sonnet 5 682ba9d64d feat: exclude per-site log/ directories from the archive
nginx access/error logs (and any other per-site log/ dir) change on
every request, so they showed up as modified in every single backup -
pure noise, no recovery value. Exclude any path containing a log/
directory (re:/log/) from borg create.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:46:35 +02:00
kbeandClaude Sonnet 5 511c95063d fix: replace undefined log() calls with echo in dump_db.sh
dump_users_and_grants() called log(), which dump_db.sh never defines
(only die()/warn() exist) - every real run printed "log: command not
found" on stderr while still completing successfully. Use echo, matching
the rest of the script's style.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:37:56 +02:00
kbeandClaude Sonnet 5 dc414de14e docs: add README, note repo is unencrypted by deliberate choice
CLAUDE.md previously said the repo was encrypted, which was true when
written but no longer matches this deployment - the operator chose to
stay unencrypted, so the recurring "not encrypted" warning is expected
behavior, not something to fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:35:54 +02:00
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
kbe b8564703bc Use root account for database 2026-07-25 21:26:10 +02:00
kbe 88696d6e30 Update chmod 2026-07-25 21:25:05 +02:00
kbe e40802deeb No more healthcheck 2026-07-25 21:23:17 +02:00
kbe 86c7731d0c Ignore files, remove old and add CLAUDE.md 2026-07-25 21:16:30 +02:00
kbeandClaude Sonnet 5 592ef45bab fix: start mariadb before restoring DBs in cmd_full, add lock/preflight
Final review of the restore.sh branch found cmd_full restored every
database via `docker exec` before starting the container, which fails
immediately in the exact scenario full restore exists for (a freshly
rebuilt, stopped container). Reorders to extract -> start container ->
restore DBs.

Also, while touching cmd_full:
- Extract directly into place (cd / && borg extract) instead of staging
  a full copy under /tmp then cp -a'ing it into $TARGET - halves disk
  usage and restore time.
- Replace `rm -rf "$TARGET"/*` with `find "$TARGET" -mindepth 1 -delete`
  so dotfiles don't survive a --force wipe.
- Add acquire_lock() (shares borg-backup.sh's lockfile so a restore and
  the nightly backup cron can't run concurrently) and preflight()
  (passphrase file readable, repo reachable) before any real work in
  full/db/file.

Test isolation: mock borg/docker/mysql/mariadb consistently via a
BASH_ENV shim (previously only db-mode's test worked around PATH
shadowing by a real docker binary; every mocked test needed it, and a
missing `flock` mock broke everything once acquire_lock was added,
since flock(1) doesn't exist on macOS). Tests also isolate LOCKFILE and
BORG_PASSPHRASE_FILE to throwaway paths.

RUNBOOK.md: fix the quarterly drill command (borg extract has no
--destination flag, and needs `borg list --short` for a bare archive
name), reword the full-restore --force comment which read backwards,
and document the MYSQL_ROOT_PASSWORD/RESTORE_LOGDIR env overrides and
where restore logs land.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:42:56 +02:00
kbe 8d7a603615 docs: add operational runbook for backup and recovery 2026-07-25 19:27:34 +02:00
kbe 4be1372e4d feat: add full disaster-recovery mode to restore.sh
Adds container_running/container_health/wait_for_container/start_db
helpers and cmd_full(), plus tests. Fixed a delimiter bug in the two
new tests' sed scripts (the literal `${TARGET#/}` replacement text
collided with `#` as the sed delimiter) and relaxed the dry-run
no-external-calls assertion to allow the legitimate borg-list call
from resolve_archive(), mirroring the existing db dry-run test.
2026-07-25 19:23:27 +02:00
kbe 5d547f04ec feat: add single-database restore mode to restore.sh 2026-07-25 19:16:13 +02:00
kbe ea361904ba feat: add non-destructive file-restore mode to restore.sh 2026-07-25 19:01:33 +02:00
kbe 769eeea2b9 feat: add archive listing and resolution to restore.sh 2026-07-25 18:57:21 +02:00
kbe 49ed87b46b feat: add restore.sh scaffold with arg parsing and usage 2026-07-25 18:54:01 +02:00
kbe d6f30c506e chore: initial import of backup scripts and design docs
Existing borg-backup.sh/dump_db.sh, old/ reference scripts, and the
brainstormed design spec for restore tooling + runbook.
2026-07-25 18:51:38 +02:00