backup-strategy.md: the two-tool architecture (incus copy + restic), ASCII map of the legs (ks4 sdb / nuc pull / two S3 buckets), the schedule table, a 30-second health check, and restore recipes for a file, a database and a whole instance. new-container.md: conventions for adding a service — one container per service, no Docker (incus runs OCI natively, incus-compose for stacks), gateway/proxy ingress, leave the snapshot schedule alone, what the backups pick up automatically vs the one line to add to restic-paths, DB-discovery requirements (incl. the .my.cnf trap), a verification run, and the pitfalls (tmpfs /tmp, tiny-file swarms, cron PATH). README: start-here links, ks4 durability bullet and flow chart updated to the restic era. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.8 KiB
ks4 backup strategy (2026-08)
Audience: anyone with root on ks4 who needs to know what is protected, where, by which tool, and how to check it. Details live in the linked docs; this page is the map.
Two tools, no more
| tool | job | why this one |
|---|---|---|
incus copy --refresh |
replicate whole instances (ready-to-start copies) | ZFS-incremental, native to the platform, restores are incus start |
| restic | off-site backups to S3 (files, database dumps, instance trees) | dedup + encryption, local metadata cache → a night costs only the churn (restic-backup.md) |
plakar was evaluated first and replaced (its nightly cost scaled with tree size — PlakarKorp/plakar#2338); the old backup server ks2 is retired (ks2/plan.md).
The map
ks4 (OVH) off-site
┌─────────────────────────────────────────────┐
│ live instances pool `data` (sda) │
│ nextcloud, seafile, mail, git, ... │
│ │ │
│ │ 01:00 incus copy --refresh │ nuc (home LAN, via WireGuard)
│ ▼ │ ┌──────────────────────────┐
│ replicas (stopped) pool `backup` (sdb) │─────▶│ 05:00 incus copy (pull) │
│ project `backup` │ │ pool `ks4backup` (USB) │
│ │ │ └──────────────────────────┘
│ │ 01:00+ restic (via incus file mount)
│ ▼ │ OVH Object Storage (S3, sbg)
│ ───────────────────────────────────────── │ ┌──────────────────────────┐
│ 05:00 restic-backup.sh │─────▶│ bucket restic-data │
│ • DB dumps (MariaDB/PostgreSQL, auto) │ │ dumps + all data trees │
│ • data trees from /root/scripts/restic-paths │ │
│ 01:00+ restic-incus-backup.sh │─────▶│ bucket restic-incus │
│ • every replica, per file + config yaml │ │ instance filesystems │
│ Sun 14:00 restic-maintenance.sh │ └──────────────────────────┘
│ • prune + check (rotating full verify) │
└─────────────────────────────────────────────┘
Copies of any given byte: live → sdb replica (same box, other disk) → nuc replica (other site) → S3 (other site, other technology). Databases additionally get application-consistent dumps nightly.
Schedule (root crontab on ks4)
| when | what | log |
|---|---|---|
| 01:00 daily | incus-copy.sh -p backup -s backup — refresh all replicas onto sdb, then restic-incus-backup.sh → S3 |
/var/log/incus-copy.log, /var/log/restic-incus.log |
| 03:00 daily | instance snapshots (incus profile, 7-day expiry) — what keeps the refreshes incremental | incus info <inst> |
| 05:00 daily | restic-backup.sh — dumps + data trees → S3 |
/var/log/restic-backup.log |
| 05:00 daily (nuc) | nuc pulls all replicas over WireGuard | nuc: /var/log/incus-copy-ks4.log |
| Sun 14:00 | restic-maintenance.sh — prune (capped), structure check, 1/52 data verification |
/var/log/restic-maintenance.log |
| 1st of month 06:00 / quarterly 15th | seafile GC dry-run report / real GC (ks4/seafile-gc.md) | /var/log/seafile-gc.log |
Retention on S3: 14 daily, 8 weekly, 6 monthly snapshots.
"Did last night work?" — the 30-second check
grep -aE "done \(rc=|FAILED" /var/log/incus-copy.log | tail -2
grep -aE "snapshot .* saved|done \(rc=|failed" /var/log/restic-backup.log | tail -3
grep -aE "done \(rc=|failed" /var/log/restic-incus.log | tail -2
rc=0 everywhere = fine. Any failed/FAILED line names the culprit
(instance, database or path). All scripts exit non-zero on any error,
never silently.
Restoring
- A whole instance, fast (same box):
incus copy backup:<inst>style — copy the replica from projectbackupback intodefault(ks4/incus-copy.md); from nuc the same via the remote. - A file or directory (any date within retention):
restic -r s3:s3.sbg.io.cloud.ovh.net/restic-data restore <snapshot> --target /backup/restore-x --include <path>(never restore into/tmp— it is RAM). Env:. /root/.restic-env. - A database: restore the
.sqldump fromrestic-data(/backup/dumps/mariadb/<inst>/<db>.sql,grants.sqlfor users), load it withincus exec <inst> -- mariadb < dump.sql. - An instance from S3 only (ks4 and both replica sites gone):
restore its tree +
<inst>.yamlfromrestic-incus, recreate the instance from the yaml/profile, push the tree back.
Secrets you need for any of this: /root/.restic-passphrase and the S3
keys in /root/.restic-env — both are in the password manager;
without the passphrase the S3 buckets are unreadable.
Adding something to the backups
New instances are picked up automatically by the replica and
instance legs (all instances, opt-out only). Databases are
auto-discovered. Only data trees need one line in
/root/scripts/restic-paths — see new-container.md.