Skip to content

Scenario 01 — Protect a stateful app, consistently

The everyday case. You run a database on a PVC. You want nightly backups that the database can actually be restored from, and a sane retention window. This is the usual reason to install Kopiur, and it takes four resources in one namespace.

It goes one step past example 01. It adds hooks that quiesce the database around the snapshot, so the captured bytes are application-consistent, not just crash-consistent.

Field Where What it does
backend.s3.bucket / prefix / endpoint / region Repository Points at your object store.
KOPIA_PASSWORD the Secret Encrypts the repository. Lose it, lose the backups.
sources[].pvc.name SnapshotPolicy The PVC to back up.
hooks.*.workloadExec SnapshotPolicy The commands that quiesce and unquiesce your app. Swap the Postgres ones for your database’s equivalents.
retention.keep* SnapshotPolicy The GFS window: how many daily, weekly, and monthly snapshots to keep.
schedule.cron / jitter SnapshotSchedule When it runs. H picks a stable minute for you.
apiVersion: v1
kind: Secret
metadata:
name: postgres-repo-creds
namespace: billing
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "REPLACE_ME"
AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
# Lose this and the backups are unrecoverable — kopia cannot decrypt without it.
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: postgres-primary
namespace: billing
spec:
backend:
s3:
bucket: my-backups
prefix: billing/postgres/
endpoint: s3.us-east-1.amazonaws.com
region: us-east-1
auth:
secretRef:
name: postgres-repo-creds
encryption:
passwordSecretRef:
name: postgres-repo-creds
key: KOPIA_PASSWORD
create:
enabled: true # this is a NEW repo — initialize it on first connect
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: postgres-data
namespace: billing
spec:
repository:
name: postgres-primary # kind defaults to Repository (same namespace)
sources:
- pvc:
name: postgres-data
# ── How the volume is captured ────────────────────────────────────────────
# Snapshot takes a point-in-time CSI VolumeSnapshot, then kopia reads THAT — no
# app downtime, consistent on disk. Combined with the hooks below you also get
# application consistency. (copyMethod defaults to Snapshot — shown explicitly
# for teaching — and needs the CSI snapshot stack + a VolumeSnapshotClass for
# your driver, ideal for databases like this; Direct reads the live volume and
# works on any storage, but set it explicitly if you have no CSI snapshots.)
copyMethod: Snapshot
# volumeSnapshotClassName: csi-hostpath-snapclass # set if not the cluster default
# ── Quiesce the database around the snapshot ──────────────────────────────
# Hooks run IN THE WORKLOAD (not the mover): tell Postgres a backup is starting
# before the snapshot, and that it finished after. A hook failure aborts the
# backup unless continueOnFailure: true.
hooks:
beforeSnapshot:
- workloadExec:
podSelector:
matchLabels:
app: postgres
container: postgres
command: ["/bin/sh", "-c", 'psql -U postgres -c "SELECT pg_backup_start(''kopiur'');"']
timeout: 2m
afterSnapshot:
- workloadExec:
podSelector:
matchLabels:
app: postgres
container: postgres
command: ["/bin/sh", "-c", 'psql -U postgres -c "SELECT pg_backup_stop();"']
# Run even if something upstream failed, so the DB never stays in
# backup mode.
continueOnFailure: true
# ── How long to keep snapshots (GFS — the only successful-retention driver) ──
retention:
keepDaily: 14
keepWeekly: 6
keepMonthly: 3
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotSchedule
metadata:
name: postgres-data-nightly
namespace: billing
spec:
policyRef:
name: postgres-data
schedule:
# Jenkins-style H pins a deterministic per-schedule minute; jitter spreads
# load so many schedules don't stampede the repository at once.
cron: "H 2 * * *"
jitter: 30m
runOnCreate: false # GitOps-friendly: don't fire the instant it's applied

The Repository should reach Ready first:

Terminal window
$ kubectl get repository -n billing
NAME PHASE AGE
postgres-primary Ready 30s

Then fire one backup by hand before you trust the schedule. That confirms the whole chain: recipe, then mover, then snapshot. This Snapshot manifest ships with the example, in its manual-snapshot section:

# Verification: fire ONE backup by hand to confirm the whole chain (recipe →
# mover → snapshot) before trusting the nightly schedule. generateName lets you
# re-apply this repeatedly to take another test snapshot.
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Snapshot
metadata:
generateName: postgres-data-test-
namespace: billing
spec:
policyRef:
name: postgres-data
Terminal window
$ kubectl apply -f deploy/examples/scenarios/01-protect-stateful-app.yaml
$ kubectl get snapshots -n billing -w
NAME PHASE ORIGIN SNAPSHOT AGE
postgres-data-test-x9f Running manual 7s
postgres-data-test-x9f Succeeded manual k1f1ec0a8 44s

A SNAPSHOT id on a Succeeded backup means the data is in the repository. From here the SnapshotSchedule takes over nightly.