Design and implement a backup strategy #41
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
There are currently no backups of anything in this cluster. The 2026-08-15 outage (#39) made that concrete: when the Postgres replica fell 42 h behind and its WAL was recycled, the only remaining copy of every database was a single Longhorn replica on one node. A
pg_dumpallwas taken by hand that day and verified — that is a snapshot someone typed, not a strategy.This issue covers designing the strategy and implementing it. Split into follow-ups once the design decisions below are settled.
Current state
backuptargets/defaultavailable: falseRecurringJobsspec.backupcloudnative-pg:1.29.2)What actually needs protecting
299.5 G is in use across 22 volumes, but a large part of it is worthless to back up.
Tier 1 — irreplaceable, ~172 G
immich/immich-libraryforgejo/forgejo-datapostgres(logical)paperless/paperless-datapaperless/paperless-mediascanservjs/scanservjs-datavault/data-vault-0vikunja/vikunja-filesTier 2 — recreatable with effort, ~1 G:
observability/grafana(dashboards),magellan-staging-*Tier 3 — derivable, explicitly exclude, ~123 G
forgejo-runner/data-forgejo-runner-{0,1,2}observability/storage-loki-0observability/prometheus-serverimmich/immich-model-cachepaperless/paperless-{redis,consume,gpt}Backing up Tier 3 would make the job 2.5× larger for no recovery value. The runner caches alone are 40 % of all used space.
Design decisions to make first
Implementation
available: trueRecurringJobs per tier, with volumes selected by label rather than by name so new PVCs are covered by default and Tier 3 has to opt out explicitlybarmanObjectStorein favour of the barman-cloud plugin — confirm which path applies before writing manifestsVerification
This is the part that usually gets skipped, and it is the only part that proves any of the above works.
Side observation
Several volumes report
actualSizelarger than their provisioned size (data-forgejo-runner-0: 47.2 G used on a 40 G volume). That is accumulated Longhorn snapshots, and it suggests snapshot cleanup is not running anywhere. Worth understanding before adding scheduled snapshots on top.Destination decided
The "no local capacity" premise in the issue body is wrong — there is a NAS with 2 × 10 TB in RAID-1 (10 TB usable) and a Hetzner Storage Box. That makes real 3-2-1 possible without buying anything.
At ~172 G of Tier 1 data into 10 TB, capacity is a non-issue onsite — retention can be generous, and the constraint moves to the Storage Box quota instead. Borg's deduplication and client-side encryption cover both the offsite retention and the encryption-at-rest requirement in one step, and Hetzner supports Borg with a dedicated endpoint. NFS clients are already present and healthy on all five nodes (
NFSClientInstalled=True), so the Longhorn side needs no host preparation — just confirm the export is NFSv4, which is what Longhorn expects.The one thing this does not solve: Postgres
The NAS speaks NFS/SMB and no S3. CNPG's barman-cloud cannot write to NFS — it only supports S3-compatible, Azure Blob and GCS. So the topology above covers every volume except the one that mattered most on 2026-08-15.
Three ways out, none of them free:
A. MinIO in-cluster, backed by an NFS share from the NAS. Gives CNPG the S3 endpoint it wants, keeps all data on the NAS. Caveat worth stating plainly: MinIO does not officially support NFS as its backend — the failure modes are locking-related and it is explicitly discouraged upstream. At this scale it will very likely work, but it is not a configuration anyone will support if it misbehaves.
B. MinIO in-cluster on a Longhorn volume. Officially supported storage for MinIO, but circular: the Postgres backup would live on the same Longhorn storage the backup exists to protect, on the same two nodes tracked in #39. Only acceptable if the Borg sync to Hetzner runs often enough to be the real backup.
C. Skip PITR. A
CronJobrunningpg_dumpallto the NFS share, plus Longhorn volume backups of the Postgres PVCs. No MinIO, no extra moving parts in the recovery path. The cost is real: RPO becomes the dump interval instead of seconds, and restore is a full logical restore. At 4.4 G of database that restore is minutes, not hours.Worth weighing against what actually happened: continuous WAL archiving is precisely what would have let the stale replica catch up instead of requiring a rebuild. That argues for A or B. Against that, every additional component in the recovery path is one more thing that can be broken at the exact moment it is needed — which is also precisely what happened on 08-15.
Where Borg runs
The NAS was described as NFS/SMB only, so it probably cannot run Borg itself. Candidates: a
CronJobin-cluster mounting the NFS export, or a systemd timer on atlas. In-cluster keeps it in GitOps and visible to the alerting from #39; on atlas it survives the cluster being down, which is the scenario backups exist for.Revised task list
backuptargets/defaultat itavailable: true, then take one manual backup and restore it before building anything on topRecurringJobs per tier (labels, not names)repokey-blake2, pruning policy matching the retention decision