Scenario 02 — Recover from accidental data loss¶
Something deleted the data. A bad migration dropped a table, a script did
rm -rf in the wrong directory, an app bug truncated a file. You have nightly
backups (scenario 01) and want yesterday's data back.
The golden rule: don't overwrite the live volume first. Restore the chosen snapshot into a new, side-by-side PVC, verify it, then cut over. An in-place overwrite into a mounted, running database is a corruption hazard — and if you picked the wrong snapshot, the original is gone too.
Step 1 — Pick the snapshot from before the incident¶
Restore is "pick a row" — no timestamp math. List the candidate Snapshot CRs for
the recipe, newest last, and choose the last one from before things broke:
$ kubectl get snapshots -n billing \
-l kopiur.home-operations.com/config=postgres-data \
--sort-by=.status.timing.startTime
NAME PHASE ORIGIN SNAPSHOT AGE
postgres-data-20260522-021300 Succeeded scheduled k1a9... 2d
postgres-data-20260523-021300 Succeeded scheduled k1f1... 1d # <- last good
postgres-data-20260524-021300 Succeeded scheduled k2c4... 2h # incident was here
Step 2 — Restore beside the original¶
The bundle below restores the chosen Snapshot into a fresh PVC
(postgres-data-recovered). The live postgres-data PVC is untouched. The
in-place variant — for once you've decided the live data is unrecoverable — is
included as a commented block at the bottom of the file.
enableFileDeletion turns the target into a mirror
A restore is additive by default: it writes the snapshot's files and leaves
anything else in the target alone. enableFileDeletion: true deletes files in
the target that aren't in the snapshot, making it an exact mirror — necessary for
a faithful in-place restore, dangerous if you point it at the wrong PVC. Use it
deliberately.
# Scenario 02 — Recover from accidental data loss (someone deleted the data)
#
# A bad migration dropped a table; a `rm -rf` took out a directory. You have
# nightly backups (scenario 01) and want yesterday's data back. The safe play is
# NOT to overwrite the live volume — it's to restore the chosen snapshot into a
# NEW, side-by-side PVC, verify it, then cut the app over. This file is that
# safe restore; the in-place variant is shown (commented) at the bottom.
#
# First, pick the snapshot from BEFORE the incident (restore is "pick a row"):
#
# kubectl get snapshots -n billing \
# -l kopiur.home-operations.com/snapshot-policy=postgres-data \
# --sort-by=.status.timing.startTime
#
# Field shapes verified against crates/api: source/target are externally tagged
# (source.snapshotRef, target.pvc creates a PVC / target.pvcRef writes an existing
# one). Explicit sources default to onMissingSnapshot: Fail (fail-closed).
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Restore
metadata:
name: postgres-recover-pre-incident
namespace: billing
spec:
source:
snapshotRef:
# The specific Snapshot CR you picked from the list above — the last good one.
name: postgres-data-20260523-021300
namespace: billing
target:
# Restore BESIDE the original into a fresh PVC; the live volume is untouched.
pvc:
name: postgres-data-recovered
storageClassName: fast-ssd
capacity: 100Gi
accessModes:
- ReadWriteOnce
options:
# Additive (the default): write the snapshot's files, leave anything already
# in the target alone. Into a fresh PVC this is moot, but it's the safe habit.
enableFileDeletion: false
policy:
onMissingSnapshot: Fail # an explicit recovery that finds nothing is an error
waitTimeout: 5m
#
# Watch it: kubectl get restore postgres-recover-pre-incident -n billing -w
# Then point the app at postgres-data-recovered (or copy the rows you need out
# of it), confirm, and delete the original / the recovered PVC as appropriate.
#
# ── In-place variant (overwrites the LIVE volume — scale the app DOWN first) ──
# Only do this once you've decided the live data is unrecoverable. Restoring into
# a mounted, running database is a corruption hazard.
#
# apiVersion: kopiur.home-operations.com/v1alpha1
# kind: Restore
# metadata:
# name: postgres-recover-in-place
# namespace: billing
# spec:
# source:
# snapshotRef:
# name: postgres-data-20260523-021300
# target:
# pvcRef:
# name: postgres-data # the EXISTING live PVC
# options:
# enableFileDeletion: true # make the volume an EXACT mirror of the snapshot
# policy:
# onMissingSnapshot: Fail
Step 3 — Watch it, then cut over¶
$ kubectl get restore postgres-recover-pre-incident -n billing -w
NAME PHASE AGE
postgres-recover-pre-incident Resolving 2s
postgres-recover-pre-incident Restoring 9s
postgres-recover-pre-incident Completed 41s
Now point the app at postgres-data-recovered (or copy the rows you need out of
it), confirm the data is what you expected, and only then retire the original.
Explicit restores fail-closed (onMissingSnapshot: Fail) — if the snapshot
you named isn't there, the restore errors instead of silently producing an empty
volume.
See also¶
- Restores — the full
source/target/options/policyreference, includingpvcRef(in-place) and theidentitysource. - Scenario 03 — disaster recovery — when it's not one volume but the whole cluster.
- Troubleshooting — if a restore won't progress.