Skip to content

Scenario 01 — Protect a stateful app, consistently

The everyday case. You run a database on a PVC and want nightly, application-consistent backups with a sane retention window. This is the canonical reason to install Kopiur, and it's 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 not just crash-consistent but application-consistent.

What you'll deploy

A Secret (backend creds + repo password), a Repository (a new S3 repo for this app), a SnapshotPolicy (the recipe — source PVC, copy method, hooks, retention), and a SnapshotSchedule (the nightly cron). All in the app's namespace, because that's where the mover Job runs.

The values you'll actually change

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/unquiesce your app. Swap the Postgres ones for your database's equivalents.
retention.keep* SnapshotPolicy The GFS window — how many daily/weekly/monthly snapshots to keep.
schedule.cron / jitter SnapshotSchedule When it runs. H picks a stable minute for you.

Why hooks instead of just a snapshot?

copyMethod: Snapshot (the default; needs the CSI snapshot stack and is the right choice for a database like this) already gives you a crash-consistent point-in-time copy. The beforeSnapshot/afterSnapshot hooks add application consistency: they run inside the workload to tell the database a backup is starting, so what's on disk at snapshot time is a clean, restorable state. A hook failure aborts the backup unless you set continueOnFailure: true (we set it on the after hook so the database never gets stuck in backup mode).

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

Verify it worked

The Repository should reach Ready first:

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

Then fire one backup by hand to confirm the whole chain (recipe → mover → snapshot) before trusting the schedule. This Snapshot manifest ships with the example (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
$ 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.

See also