Add automatic backup to PVC(s) in $ARGUMENTS.
Backups are kopiur (replaced pvc-plumber + VolSync, retired 2026-06-27).
Each PVC gets a small SnapshotPolicy + SnapshotSchedule + Restore stub plus
the shared my-apps/common/kopiur-backup Kustomize component; kopiur runs the
Snapshot/Restore Jobs and kopia moves bytes to RustFS (s3://kopiur). Do NOT add
inline ReplicationSource/ReplicationDestination or pvc-plumber labels — those
are gone.
Architecture + diagrams:
docs/domains/storage/kopiur-backup-architecture.md. Why the mover runs as the data owner:docs/domains/storage/kopiur-mover-permissions.md.
-
Identify the normal application PVCs that need protection. Confirm each uses
storageClassName: longhorn(needs CSI VolumeSnapshot). -
Find the data owner uid:gid — the mover MUST run as it (under baseline Pod Security a root mover can't read non-root/600/700 files):
pod=$(kubectl -n <ns> get pod -l app.kubernetes.io/name=<app> -o name | head -1) kubectl -n <ns> exec "${pod#pod/}" -- stat -c '%u:%g' <data-mountpath> # also check files, not just the mount root (daemon-drop apps differ): find ... -printf '%u:%g\n'
-
Namespace — one label (creds fanout + repo tenancy). Add the annotation ONLY if the data owner is
0(root):metadata: labels: kopiur.home-operations.com/repo: cluster-kopia # annotations: # root-owned data only # kopiur.home-operations.com/privileged-movers: "true"
-
Per-PVC stub
kopiur/<pvc>.yaml— varying bits only; mover = data owner:--- apiVersion: kopiur.home-operations.com/v1alpha1 kind: SnapshotPolicy metadata: { name: <pvc>, namespace: <ns> } spec: sources: [{ pvc: { name: <pvc> } }] identity: { username: <pvc>, hostname: <ns> } retention: { keepDaily: 14, keepWeekly: 6, keepMonthly: 3 } # hourly: keepHourly:24,keepDaily:7,keepWeekly:4 mover: securityContext: { runAsUser: <UID>, runAsGroup: <GID>, runAsNonRoot: true } # root: {runAsUser:0,runAsNonRoot:false} podSecurityContext: { fsGroup: <GID>, supplementalGroups: [<GID>] } --- apiVersion: kopiur.home-operations.com/v1alpha1 kind: SnapshotSchedule metadata: { name: <pvc>-daily, namespace: <ns> } spec: { policyRef: { name: <pvc> }, schedule: { cron: "MM 3 * * *" } } # distinct minute vs ALL schedules — incl. hourly "MM * * * *" tiers (an hourly at :MM collides with a daily at 03:MM) # Taken minutes: grep -rh 'cron:' my-apps/*/*/kopiur* my-apps/*/*/*/kopiur* | sort --- apiVersion: kopiur.home-operations.com/v1alpha1 kind: Restore metadata: { name: <pvc>-restore, namespace: <ns> } spec: source: { fromPolicy: { name: <pvc>, offset: 0 } } mover: securityContext: { runAsUser: <UID>, runAsGroup: <GID>, runAsNonRoot: true } podSecurityContext: { fsGroup: <GID>, supplementalGroups: [<GID>] }
The component injects the uniform fields (
repository: cluster-kopia,copyMethod: Snapshot,volumeSnapshotClassName: longhorn-snapclass,deletion.onPolicyDelete: Retain,target.populator: {},onMissingSnapshot: Continue, scheduleconcurrencyPolicy: Forbid/runOnCreate: false) — do not duplicate them. -
Kustomization — add the stub + the component:
resources: - kopiur/<pvc>.yaml components: - ../../common/kopiur-backup
-
PVC — restore-before-bind via
dataSourceRef, keep the masking annotations:metadata: annotations: argocd.argoproj.io/compare-options: ServerSideDiff=false argocd.argoproj.io/sync-options: ServerSideApply=false spec: dataSourceRef: apiGroup: kopiur.home-operations.com kind: Restore name: <pvc>-restore
(Helm-rendered PVC: inject the
dataSourceRef+ annotations via a Kustomizepatches:block on the chart PVC — seemy-apps/development/gitea/.)Existing/Bound PVC? ArgoCD will show a
PVC is invalid: ForbiddenComparisonError —dataSourceRefis immutable on a Bound PVC. This is EXPECTED and harmless: backups start immediately; thedataSourceRefarms on the next recreate (i.e., at DR time). The masking annotations + AppSetignoreDifferenceshandle the diff — do NOT try to "fix" it by recreating the PVC unless you actually want a restore drill. -
Sync through GitOps and verify:
kubectl -n <ns> get snapshotpolicy,snapshotschedule,restore kubectl -n <ns> get secret kopiur-rustfs # fanned in by the ClusterExternalSecret kubectl -n <ns> get snapshot # Succeeded with non-zero files after first run
Do not back up:
- Redis PVCs — backup-exempt, disposable.
- PostHog ClickHouse/Kafka/Redis PVCs — backup-exempt, disposable. Postgres is kopiur-backed because it holds identity and configuration; do not exempt it.
- System-namespace PVCs.
- Non-Longhorn PVCs that can't use the CSI snapshot path.
For intentionally disposable PVCs, label backup-exempt: "true" + the
fully-qualified annotation storage.vanillax.dev/backup-exempt-reason: "<reason>".