Copy methods: Snapshot, Clone, Direct¶
SnapshotPolicy.spec.copyMethod chooses how Kopiur captures your data before kopia reads it. Kopia always backs up files from a mounted volume — the copy method only decides which volume the backup mover mounts:
| Method | What the mover reads | Point-in-time? | Decoupled from the app's node? | Requires |
|---|---|---|---|---|
Snapshot (default) |
A temporary PVC restored from a CSI VolumeSnapshot of your source | ✅ yes | ✅ yes | CSI snapshot stack + a VolumeSnapshotClass |
Clone |
A temporary CSI clone of your source PVC | ✅ yes (at clone time) | ✅ yes | CSI driver with volume-clone support |
Direct |
Your live source PVC, read-only | ❌ no (crash-consistent live read) | ❌ no (co-located with the app) | Nothing — works on any storage |
Which should I use?¶
Do you have (or can you install) the CSI snapshot stack for this source?
│
├─ Yes ─────────────────────────────────────────────► Snapshot (default, preferred)
│ (crash-consistent, point-in-time, decoupled from the app's node)
│
│ Only volume cloning, not snapshots? ────► Clone
│
└─ No — no CSI snapshot support, or a static/hostPath/non-CSI source ─► Direct
(config, media, file shares; simplest; works everywhere; set explicitly)
- Start with
Snapshot(the default) — crash-consistent, point-in-time, and decoupled from the node your app runs on. Best for databases and anything you don't want tied to app placement. - Set
copyMethod: Directexplicitly if you don't have (or don't want to maintain) the CSI snapshot stack, or the source is a static/non-CSI volume (hostPath, some NFS setups) — it works on any storage, no CSI required. - Use
Cloneonly if your driver does cloning but not snapshots (uncommon).
Snapshot is the default
copyMethod defaults to Snapshot because it is crash-consistent: kopia reads a frozen point-in-time capture instead of a live, possibly-mid-write PVC — the difference matters most for databases and other stateful apps. It requires the CSI snapshot stack (external-snapshotter + a VolumeSnapshotClass for your source's driver). If your cluster doesn't have it, or the source is a static/non-CSI volume, set copyMethod: Direct explicitly. When the stack/class is missing and copyMethod was left at its default, the backup fails with a clear condition telling you exactly what to install or which field to set — it never silently falls back to a live read.
Upgrading? Two hazards when copyMethod is left implicit
copyMethod began defaulting to Snapshot as of this release (previously Direct). Check any SnapshotPolicy that never sets copyMethod explicitly:
- No CSI snapshot stack for that source? It now fails instead of silently reading the live PVC — pin
copyMethod: Directon that policy. - Server-side re-defaulting: a server-defaulted field has no field owner under server-side apply. Re-applying an existing manifest that omits
copyMethodcan silently flip a storedDirectvalue toSnapshotonce the CRD is upgraded — pincopyMethodexplicitly on everySnapshotPolicyyou manage, especially ones reconciled by GitOps. This also applies to manifests previously produced bykopiur-migrate: translations run before this release omitcopyMethodwhen the source VolSync object had none set, so they're exposed to this hazard too — re-run the migration (now emits an explicit value) or addcopyMethod: Directby hand before re-applying.
Try it end-to-end¶
See copyMethod: Snapshot actually stage a CSI copy: the bundle deploy/examples/tryit/copy-methods.yaml is self-contained — namespace, a filesystem Repository on a PVC, a CSI-provisioned app-data PVC, a seed Job, a copyMethod: Snapshot policy, and a fixed-name Snapshot (app-data-snapshot) so the staged PVC is deterministically app-data-snapshot-src.
The load-bearing line is copyMethod: Snapshot on the policy — it tells the mover to read a staged CSI copy instead of the live volume:
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: app-data
namespace: kopiur-tryit
spec:
repository:
name: primary
# The load-bearing line: stage a CSI VolumeSnapshot instead of reading live.
copyMethod: Snapshot
# volumeSnapshotClassName: csi-... # optional — omit to use the driver default
sources:
- pvc:
name: app-data
retention:
keepDaily: 7
keepWeekly: 4
Prerequisite: this bundle needs the CSI snapshot stack
copyMethod: Snapshot snapshots the source PVC, so the source must be CSI-provisioned and the cluster must have the external-snapshotter (the snapshot-controller + VolumeSnapshot/VolumeSnapshotContent/VolumeSnapshotClass CRDs) and a VolumeSnapshotClass whose driver matches your source's StorageClass provisioner. Fill in both REPLACE_ME values: storageClassName (a CSI class that supports snapshots) and KOPIA_PASSWORD. Leave volumeSnapshotClassName unset to auto-pick the driver's default class.
1. Apply and wait for the prerequisites.
$ kubectl apply -f deploy/examples/tryit/copy-methods.yaml
$ kubectl -n kopiur-tryit wait --for=condition=complete job/seed-data --timeout=2m
$ kubectl -n kopiur-tryit wait --for=condition=Ready repository/primary --timeout=2m
2. While the backup is Running, catch the staged PVC. The mover reads app-data-snapshot-src, not the live app-data. Fetch it by name (not a label) while the snapshot is in flight:
$ kubectl -n kopiur-tryit get snapshot app-data-snapshot -w &
$ kubectl -n kopiur-tryit get pvc app-data-snapshot-src
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
app-data-snapshot-src Bound pvc-.. 1Gi RWO 6s
3. After success, read status.staged (deep). Wait for the terminal phase, then confirm the run staged a CSI copy and which PVC the mover mounted:
$ kubectl -n kopiur-tryit wait --for=jsonpath='{.status.phase}'=Succeeded \
snapshot/app-data-snapshot --timeout=5m
$ kubectl -n kopiur-tryit get snapshot app-data-snapshot \
-o jsonpath='{.status.staged}'
{"copyMethod":"Snapshot","volumeSnapshotName":"app-data-snapshot-...","pvcName":"app-data-snapshot-src","ready":true,"storageClassName":"...","stagingTimeoutSeconds":600}
(Illustrative: volumeSnapshotName is generated and storageClassName is your class; the rest is exact.)
4. Confirm the stage was reaped and the live PVC was never mounted. Kopiur cleans up the staged objects on completion:
$ kubectl -n kopiur-tryit get pvc app-data-snapshot-src
Error from server (NotFound): persistentvolumeclaims "app-data-snapshot-src" not found
The live app-data PVC was never mounted by the mover — only the staged copy was. To tear down: kubectl delete namespace kopiur-tryit.
Snapshot — point-in-time CSI snapshot (default)¶
When a backup runs, Kopiur:
- Creates a CSI
VolumeSnapshotof your source PVC (after anybeforeSnapshothooks, so a quiesced app yields a consistent capture). - Waits for the snapshot to become
readyToUse— bounded by the staging deadline (spec.staging.timeout, default10m). - Provisions a temporary staged PVC from the snapshot.
- Runs the kopia mover against the staged PVC — never the live volume.
- Cleans everything up (staged PVC + VolumeSnapshot) when the backup finishes.
The staged PVC is brand-new and unheld, so the backup mover schedules freely — it is fully decoupled from the node your application runs on (unlike Direct, which must co-locate).
What it requires¶
Snapshot needs the cluster's CSI snapshot stack, which your cluster administrator installs once:
- The external-snapshotter — the
snapshot-controllerDeployment and theVolumeSnapshot/VolumeSnapshotContent/VolumeSnapshotClassCRDs (see the kubernetes-csi external-snapshotter docs). Many managed distributions (EKS, GKE, AKS, Talos, k3s add-ons) ship or offer this. - A
VolumeSnapshotClasswhosedrivermatches the CSI provisioner of your source PVC'sStorageClass.
If your distribution does not bundle a snapshot-controller, the home-operations snapshot-controller chart installs the controller and the snapshot CRDs (vendored byte-for-byte from the upstream external-snapshotter release):
helm install snapshot-controller oci://ghcr.io/home-operations/charts/snapshot-controller \
--namespace kube-system
The same chart can create the VolumeSnapshotClass for you: add an entry under its volumeSnapshotClasses value naming the driver for your source's storage (optionally annotated snapshot.storage.kubernetes.io/is-default-class: "true"), so the whole prerequisite lands in one install. Skip the chart on a distribution that already runs a controller (EKS, GKE, AKS, Talos, k3s add-ons); a second one just contends for the same CRs.
Helm never upgrades the snapshot CRDs
Like Kopiur's own chart, this one ships the CRDs in its crds/ directory: helm install creates them, but helm upgrade leaves them untouched. After bumping the chart across an appVersion, reapply the matching CRDs yourself:
If any of this is missing, the backup fails with a clear condition telling you exactly what to do — Kopiur never silently downgrades a Snapshot backup to a live read. See Troubleshooting below.
Choosing the VolumeSnapshotClass¶
spec:
copyMethod: Snapshot
# Optional. Leave unset to auto-select your driver's DEFAULT class.
volumeSnapshotClassName: csi-rbd-snapclass
- Set it explicitly to pin a specific class.
- Leave it unset and Kopiur picks the default
VolumeSnapshotClassfor your source's driver (the one annotatedsnapshot.storage.kubernetes.io/is-default-class: "true"). If exactly one class exists for the driver it's used even without the annotation. - If no class matches your driver, or several match with no single default, the backup fails asking you to create/annotate a class or name one explicitly.
How long staging may wait (spec.staging.timeout)¶
Staging has a deadline budget that bounds each of its phases:
- The
VolumeSnapshotbecomingreadyToUse— measured from the VolumeSnapshot's creation. - The staged PVC binding — a fresh budget measured from the staged PVC's creation. On an
Immediate-binding StorageClass the CSI restore/clone runs at provision time and Kopiur waits forBoundbefore creating the mover Job (so a slow restore can never strand an unschedulable mover, and the VolumeSnapshot is never torn down under an in-flight restore). On aWaitForFirstConsumerclass the bind happens when the mover pod schedules — Kopiur then keeps watching the staged PVC while the Job runs and fails the backup with the same reason if the bind blows the budget.
spec:
copyMethod: Snapshot
staging:
# Go-style duration; default 10m. "0" waits indefinitely.
timeout: 30m
- Default
10m— plenty for drivers that cut snapshots in seconds (most local/on-cluster CSI), and bounded so a broken driver can't hold aSnapshotPendingforever and silently starve aconcurrencyPolicy: Forbidschedule. - Raise it for backends whose snapshots or restores take long — e.g. cloud snapshots of large volumes (the first EBS snapshot of a big volume can take well over 10 minutes), or a CephFS full-clone restore of a small-file-heavy volume (see staging overrides for the shallow-clone alternative that makes it near-instant instead).
timeout: "0"waits indefinitely (never fails on the deadline).
Only this deadline fails staging. If it expires the backup goes Failed with reason VolumeSnapshotFailed (the snapshot was still reporting an error), StagingTimedOut (no error — the driver/snapshot-controller is stuck), or StagedPvcBindTimeout (the snapshot was fine but the staged PVC never bound — the restore/clone is still provisioning or can't provision), and the message names this field. A Failed backup is terminal; the next scheduled run (or a new Snapshot) retries — and Kopiur reaps whatever staging objects the failed run already created.
A VolumeSnapshot error during the wait is NOT a failure
The snapshot-controller routinely reports transient errors on a perfectly healthy VolumeSnapshot — most commonly a benign 409 Conflict ("the object has been modified; please apply your changes to the latest version and try again") while it adds finalizers, which its own retry clears a moment later. Kopiur surfaces such errors on the SourceStaged condition for visibility but keeps waiting; it declares VolumeSnapshotFailed only if the snapshot is still not readyToUse when the staging deadline passes.
# Example 21 — copyMethod: Snapshot (point-in-time CSI VolumeSnapshot)
#
# THE DEFAULT copyMethod (shown explicitly here for clarity) — recommended for
# stateful apps whenever you have the CSI snapshot stack. Kopiur takes a CSI
# VolumeSnapshot of the source PVC, restores
# it into a temporary staged PVC, and runs the mover against that stage — never the live
# volume. The backup is
# point-in-time and fully decoupled from the node your app runs on; the staged
# objects are cleaned up automatically when the backup finishes.
#
# REQUIRES the cluster CSI snapshot stack (external-snapshotter + CRDs) and a
# VolumeSnapshotClass whose driver matches the source PVC's StorageClass provisioner.
# Without it the backup fails with a clear, actionable condition (it never silently
# falls back). See docs/copy-methods.md.
#
# Leave volumeSnapshotClassName UNSET to auto-select your driver's default class;
# set it (as below) to pin a specific one.
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: postgres
namespace: databases
spec:
repository:
name: primary
copyMethod: Snapshot
volumeSnapshotClassName: csi-rbd-snapclass # optional; omit to use the driver default
# Optional: the staging deadline budget (default 10m; "0" waits indefinitely).
# Bounds the VolumeSnapshot becoming readyToUse AND (a fresh budget) the staged
# PVC's bind — the CSI restore window. Transient CSI/snapshot-controller errors
# during the wait are retried — only this deadline fails staging. Raise it for
# backends whose snapshots or restores are slow (e.g. cloud snapshots of large
# volumes). `staging` also takes storageClassName/accessModes overrides for the
# staged PVC — see example 30 (CephFS backingSnapshot shallow staging).
staging:
timeout: 10m
sources:
- pvc:
name: postgres-data
retention:
keepDaily: 14
keepWeekly: 8
# For application consistency, quiesce the database before the snapshot is taken
# (the VolumeSnapshot is captured AFTER beforeSnapshot hooks).
hooks:
beforeSnapshot:
- workloadExec:
podSelector:
matchLabels:
app.kubernetes.io/name: postgres
command: ["/bin/sh", "-c", "psql -c 'CHECKPOINT;'"]
continueOnFailure: false
Clone — CSI volume clone¶
Clone provisions the staged PVC directly from your source PVC (dataSource: PersistentVolumeClaim) — a CSI volume clone — with no intermediate VolumeSnapshot. Like Snapshot, the mover reads the clone and the clone is cleaned up afterward.
Use it when your CSI driver supports cloning (CLONE_VOLUME) but not snapshots. It needs no VolumeSnapshotClass.
# Example 22 — copyMethod: Clone (CSI volume clone)
#
# Stages the backup from a CSI CLONE of the source PVC (dataSource: the source PVC
# itself) instead of a VolumeSnapshot. Use this when your CSI driver supports volume
# cloning (CLONE_VOLUME) but not snapshots. Like Snapshot, the mover reads the clone
# and the clone is cleaned up afterward — no VolumeSnapshotClass is needed.
#
# If the driver can't clone the volume, the staged PVC stays Pending and the backup
# never starts — check `kubectl describe pvc <snapshot-name>-src` and switch to
# Snapshot or Direct. See docs/copy-methods.md.
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: app-data
namespace: apps
spec:
repository:
name: primary
copyMethod: Clone
sources:
- pvc:
name: app-data
retention:
keepDaily: 7
Clone requires driver support
If your driver can't clone the volume, the staged PVC stays Pending and the backup fails with StagedPvcBindTimeout once the staging deadline passes. If you see that, check the staged PVC's events (kubectl describe pvc <snapshot-name>-src) and use Snapshot or Direct instead.
Staging overrides¶
By default the staged PVC copies its storageClassName and accessModes from the source PVC. Two optional spec.staging fields override that — for the staged PVC only; your application's PVC is never touched:
spec:
copyMethod: Snapshot # overrides apply to Snapshot AND Clone
staging:
storageClassName: cephfs-backingsnapshot # class for the STAGED PVC
accessModes: [ReadOnlyMany] # modes for the STAGED PVC
storageClassName— stage on a different class of the same CSI driver, typically one with different restore parameters. Kopiur verifies the driver matches up front and fails fast withStagedClassMismatchif it doesn't (a foreign driver can never provision from your source's snapshot — without the check you'd get an opaque bind timeout instead).accessModes— request different modes for the stage (e.g.[ReadOnlyMany]for a snapshot-backed read-only class). The mover mounts the staged source read-only unless the source setsreadOnly: false— and[ReadOnlyMany]is rejected together with that, since a read-only stage cannot be mounted read-write.- Both are meaningless without a staged PVC, so they're rejected at admission for
copyMethod: Direct, NFS sources, andpvcSelectorsources.
The flagship use: CephFS shallow snapshots¶
On CephFS, restoring a VolumeSnapshot into a staged PVC is a full subvolume clone — an MDS-metadata-bound copy of every file. For a volume with many small files (a git server's loose objects, maildirs), that clone can take many minutes even at ~100 MB, blowing the staging budget on setup. ceph-csi's answer is a StorageClass with backingSnapshot: "true": restore-from-snapshot then mounts the snapshot shallowly — metadata-only, read-only, ready in seconds. Point spec.staging.storageClassName at such a class and the whole problem disappears:
# Example 30 — CephFS shallow snapshot staging (spec.staging.storageClassName)
#
# copyMethod: Snapshot normally restores the CSI VolumeSnapshot into a staged PVC
# on the SOURCE's StorageClass. On CephFS that restore is a FULL subvolume clone —
# metadata-bound, so a volume with many small files (git servers, maildirs,
# node_modules-shaped data) can take many minutes to clone even at ~100 MB, and a
# slow clone burns the whole staging budget doing setup instead of backup.
#
# The fix: point the STAGED PVC (and only it — your app's PVC is untouched) at a
# rook-ceph StorageClass with `backingSnapshot: "true"`. ceph-csi then mounts the
# snapshot SHALLOWLY — a metadata-only, read-only view, ready in seconds — instead
# of copying every file. Shallow volumes are read-only by design, hence
# `accessModes: [ReadOnlyMany]`; the kopia mover always mounts the staged source
# read-only anyway, so nothing else changes.
#
# Maps to SnapshotPolicy.spec.staging.{storageClassName,accessModes}
# (crates/api/src/snapshot_policy.rs::StagingSpec). Both fields apply to
# copyMethod Snapshot AND Clone; absent they inherit from the source PVC.
# The override class MUST belong to the same CSI driver as the source — kopiur
# fails fast with `StagedClassMismatch` if it doesn't.
#
# Requires: rook-ceph / ceph-csi with CephFS (shallow volumes need ceph-csi >= 3.7)
# and the CSI snapshot stack. Adjust clusterID/fsName/secret names to your rook
# install (these are the rook-ceph defaults).
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: cephfs-backingsnapshot
provisioner: rook-ceph.cephfs.csi.ceph.com # SAME driver as your source PVC's class
parameters:
clusterID: rook-ceph
fsName: ceph-filesystem
# The load-bearing line: restore-from-snapshot mounts the snapshot shallowly
# (metadata-only, read-only, near-instant) instead of a full subvolume clone.
backingSnapshot: "true"
csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete
allowVolumeExpansion: false
volumeBindingMode: Immediate
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: forgejo
namespace: forgejo
spec:
repository:
name: primary # an existing Repository/ClusterRepository (see examples 01-03)
copyMethod: Snapshot
staging:
# Stage on the shallow class above instead of the source's own class.
storageClassName: cephfs-backingsnapshot
# Shallow CephFS volumes are read-only; request them as ReadOnlyMany.
accessModes: [ReadOnlyMany]
# With a shallow mount the stage is ready in seconds, so the default 10m
# budget is generous. Without the override, a CephFS full clone of a
# small-file-heavy volume can need MORE than 10m — if you must full-clone,
# raise this instead (it bounds both the VolumeSnapshot readyToUse wait and
# the staged PVC's bind).
timeout: 10m
sources:
- pvc:
name: forgejo-data
retention:
keepDaily: 14
keepWeekly: 8
Same driver, shallow-capable versions, delete order
The shallow class must use the same provisioner as your source's class (kopiur enforces this). CephFS shallow volumes need ceph-csi ≥ 3.7 and are read-only by design — request them ReadOnlyMany. ceph-csi reference-tracks the backing snapshot, and Kopiur always deletes the staged PVC before the VolumeSnapshot, so the cleanup order is safe for shallow mounts.
Direct — read the live volume (opt-in)¶
Direct mounts your live source PVC into the mover, read-only, and kopia reads it in place. No snapshot, no clone, no extra storage — it works on any storage, including local-path/hostPath that has no snapshot support. Set copyMethod: Direct explicitly on the SnapshotPolicy to opt in — it is no longer the default.
Because the live volume is mounted, Kopiur co-locates the mover on the node already holding the PVC (for ReadWriteOnce volumes), avoiding the Kubernetes Multi-Attach error. See Repositories → sourceColocation. A ReadWriteOncePod source is stricter: it can't be co-mounted by the mover at all while your app holds it, so use Snapshot (or Clone) for those — see PVC access modes & RWOP.
# Example 23 — copyMethod: Direct (read the live volume) — OPT-IN
#
# Direct is opt-in (copyMethod defaults to Snapshot) — set it explicitly, as here,
# when you don't have (or don't want to maintain) the CSI snapshot stack, or the
# source is a static/non-CSI volume. Mounts the LIVE source PVC into the mover
# (read-only) and kopia reads it in place — no snapshot, no clone, no extra storage.
# Works on ANY storage, including local-path / hostPath with no snapshot support.
# The backup is crash-consistent (a live read), which is fine for config, media, and
# file shares.
#
# For ReadWriteOnce volumes Kopiur automatically co-locates the mover on the node
# already holding the PVC, avoiding the Kubernetes "Multi-Attach error" — no extra
# config needed (see docs/repositories.md#sourcecolocation-avoid-the-rwo-multi-attach-error).
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: media
namespace: media
spec:
repository:
name: primary
copyMethod: Direct
sources:
- pvc:
name: media-library
retention:
keepDaily: 7
keepWeekly: 4
Direct reads a live filesystem, so the backup is crash-consistent — fine for most file data, but for a busy database prefer Snapshot, or quiesce the app with hooks (below).
Making fsGroup apply to the source¶
By default the mover mounts the source read-only — kopia only ever reads it. That default has one surprising consequence: fsGroup does nothing on the source.
fsGroup is not a passive grant. The kubelet implements it by walking the volume and rewriting it: chgrp every file to the group, chmod g+rw, setgid on directories. That rewrite is the whole mechanism — and the kubelet skips it entirely on a read-only mount. So a mover podSecurityContext.fsGroup (or fsGroupChangePolicy) has no effect on a backup source, no matter what you set it to.
That matters when the mover must run as a specific uid:gid for reasons of its own — a non-ID-squashed NFS repository export, say — and the source PVC's files are owned by someone else. Set readOnly: false on the source and the kubelet does the walk:
spec:
copyMethod: Snapshot # the stage is what gets rewritten — see below
mover:
podSecurityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
sources:
- pvc: { name: app-data }
readOnly: false
What gets rewritten depends entirely on copyMethod:
copyMethod |
The mover mounts | readOnly: false rewrites |
|---|---|---|
Snapshot / Clone |
a temporary staged PVC | the throwaway stage, deleted when the run ends. Your volume is never touched. |
Direct |
your live PVC | your production data — permanently, while the app runs. |
So under Snapshot/Clone this is free, and it is the combination to reach for:
# Example 31 — make fsGroup apply to the backup source (sources[].readOnly: false)
#
# WHY THIS EXISTS: fsGroup is not a passive permission grant. The kubelet implements
# it by walking the volume and rewriting it — chgrp every file to the group, chmod
# g+rw, setgid on directories. And the kubelet SKIPS that walk entirely on a
# read-only mount. Kopiur mounts backup sources read-only by default (kopia only
# reads them), so mover.podSecurityContext.fsGroup / fsGroupChangePolicy are
# silently INERT on the source no matter what you set them to.
#
# That bites when the mover must run as a specific uid:gid for reasons of its own —
# here, a filesystem repository on an NFS export with no ID squashing, which only
# accepts writes from 1000:1000 — while the source PVC's files are owned by someone
# else. Set readOnly: false and the kubelet does the walk, normalizing group
# ownership so the mover can read.
#
# WHAT GETS REWRITTEN depends entirely on copyMethod:
#
# Snapshot / Clone -> the temporary STAGED PVC. Deleted when the run ends.
# Your volume is never touched. (This example.)
# Direct -> your LIVE production volume, permanently, while the app
# runs. Requires acknowledgeLiveMutation: true — see
# example 32.
#
# So pair readOnly: false with a staged copyMethod whenever your storage supports
# it; that is the combination with no downside.
#
# NOT SUPPORTED on an `nfs:` SOURCE: the kubelet never applies fsGroup to in-tree
# NFS volumes, so readOnly: false cannot achieve anything there and is rejected at
# admission. (The NFS below is the REPOSITORY, not the source.) Nor with
# staging.accessModes: [ReadOnlyMany] — a read-only stage cannot be mounted rw.
#
# See docs/copy-methods.md#making-fsgroup-apply-to-the-source
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: nas
namespace: media
spec:
backend:
filesystem:
path: /repo
nfs:
server: nas.internal
path: /export/kopia
encryption:
passwordSecretRef:
name: kopia-repo
key: KOPIA_PASSWORD
moverDefaults:
# The export is not ID-squashed, so every kopia process touching it must be
# 1000:1000 or the repository writes are refused.
securityContext:
runAsUser: 1000
runAsGroup: 1000
podSecurityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: photos
namespace: media
spec:
repository:
name: nas
# The stage is what readOnly: false rewrites — a throwaway PVC, not your data.
copyMethod: Snapshot
sources:
- pvc:
name: photos-data
# Without this, the mount is read-only, the kubelet skips the fsGroup walk,
# and the mover (uid 1000) cannot read files owned by the app's uid.
readOnly: false
retention:
keepDaily: 7
keepWeekly: 4
Direct + readOnly: false rewrites your live data
With Direct there is no stage: the kubelet chgrp's your running application's files to the mover's fsGroup and makes them group-writable — permanently, on the next backup. Postgres and Redis both refuse to start on an over-permissive data directory; anything asserting on group ownership can break the same way. There is no undo.
Because that is not inferable from intent — you set one flag to fix a permission error, not to re-own your data — Kopiur rejects the combination at admission unless you say so explicitly with acknowledgeLiveMutation: true.
Prefer copyMethod: Snapshot/Clone if your storage supports it. acknowledgeLiveMutation is ignored anywhere it is not needed, so it is safe to leave in place if you switch back.
The acknowledged form, for storage with no CSI snapshot support:
# Example 32 — copyMethod: Direct + readOnly: false — REWRITES YOUR LIVE VOLUME
#
# READ THIS BEFORE COPYING. Example 31 is almost certainly what you want instead.
#
# readOnly: false asks the kubelet to apply fsGroup to whatever the mover mounts.
# Under copyMethod: Snapshot/Clone that is a throwaway staged PVC and it costs you
# nothing. Under Direct there IS no stage — the mover mounts your live PVC, so the
# kubelet recursively chgrp's YOUR RUNNING APPLICATION'S FILES to fsGroup and makes
# them group-writable. Permanently. On the next backup.
#
# Postgres and Redis both refuse to start on an over-permissive data directory.
# Anything that asserts on group ownership can break the same way. There is no undo.
#
# Kopiur rejects this combination at admission unless you set
# acknowledgeLiveMutation: true, because the intent is not inferable: you would be
# setting readOnly: false to fix a permission error, not to re-own your data.
#
# ONLY use this when your storage genuinely cannot do CSI snapshots or clones (so
# copyMethod: Snapshot/Clone is unavailable) AND you have confirmed your workload
# tolerates the group-ownership change.
#
# acknowledgeLiveMutation is IGNORED anywhere it is not needed, so it is safe to
# leave in place if you later switch to copyMethod: Snapshot — no two-step edit.
#
# See docs/copy-methods.md#making-fsgroup-apply-to-the-source
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: SnapshotPolicy
metadata:
name: legacy-share
namespace: files
spec:
repository:
name: nas
# No CSI snapshot stack on this storage class, so there is no stage to interpose.
copyMethod: Direct
mover:
podSecurityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
sources:
- pvc:
name: legacy-share-data
readOnly: false
# "I understand the kubelet will chgrp legacy-share-data's contents to 1000
# and make them group-writable, and this workload tolerates that."
acknowledgeLiveMutation: true
retention:
keepDaily: 7
Two more rejections you may hit, both at admission:
nfssources. The kubelet does not applyfsGroupto in-tree NFS volumes at all, soreadOnly: falsecannot achieve anything there and only makes the export writable. Usemover.podSecurityContext.supplementalGroups/mover.securityContext.runAsUsermatching the export's ownership, or remap IDs server-side. See Security context.staging.accessModes: [ReadOnlyMany](and read-only staged classes generally, like CephFSbackingSnapshot). A read-only stage cannot be mounted read-write — the kubelet would fail the mount at backup time.
Even with all this correct, whether the kubelet actually performs the walk still depends on your CSI driver: fsGroupPolicy: None skips it, and the default ReadWriteOnceWithFSType skips RWX volumes. Kopiur therefore never claims SecurityContextCompatible=True on an fsGroup basis — it reports Unknown and lets the mover's readability preflight be the arbiter at runtime.
Consistency: what each method guarantees¶
Snapshot/Clonecapture a point-in-time image at the block level — crash-consistent (like a power-cut: the filesystem is intact, in-flight writes may not be flushed).Directreads files while the app may be writing — also crash-consistent, but spread across the read rather than a single instant.
For application consistency (a database flushed and quiesced), use SnapshotPolicy.spec.hooks to quiesce before the capture and resume after. With Snapshot, the VolumeSnapshot is taken after your beforeSnapshot hooks, so a FLUSH/fsfreeze hook yields a consistent snapshot. See Backups → hooks.
Cleanup & cost¶
Kopiur reaps the staged PVC and VolumeSnapshot when the backup reaches a terminal state (and again if you delete the Snapshot). To avoid the well-known leak where a Retain StorageClass leaves the staged PV (and its backend volume) behind, Kopiur flips a bound staged PV's reclaim policy to Delete before removing it. A Retain VolumeSnapshotClass keeps the underlying storage snapshot after the VolumeSnapshot object is deleted — prefer a Delete deletion policy for the class you point Kopiur at, unless you want to keep raw storage snapshots yourself.
status.staged on the Snapshot records what was created (the VolumeSnapshot + staged PVC names) for visibility.
Troubleshooting¶
If a SnapshotPolicy never sets copyMethod and the cluster has no CSI snapshot stack, the backup fails immediately on the (default) Snapshot attempt — most often with SnapshotStackMissing below. The fix in every row is the same shape: install what's missing, or pin copyMethod: Direct on the policy to opt out of CSI staging.
| Condition / symptom | Cause | Fix |
|---|---|---|
SourceStaged=False, reason SnapshotStackMissing |
No VolumeSnapshotClass API — the external-snapshotter isn't installed. |
Install the snapshot-controller and a VolumeSnapshotClass (What it requires has the snapshot-controller chart command), or set copyMethod: Direct. |
SourceStaged=False, reason NoVolumeSnapshotClass |
No class matches your source PVC's driver (or several do with no single default). | Create/annotate a VolumeSnapshotClass for the driver, set volumeSnapshotClassName explicitly, or use Direct. |
SourceStaged=False, reason VolumeSnapshotFailed |
The VolumeSnapshot was still reporting an error when the staging deadline passed (spec.staging.timeout, default 10m) — transient errors during the wait are retried, never fatal on their own. |
Read the message (it includes the driver's last error); fix the class/driver, or raise spec.staging.timeout if the backend is just slow. The next scheduled run (or a new Snapshot) retries. |
SourceStaged=False, reason StagingTimedOut |
The VolumeSnapshot never became readyToUse within the staging deadline and reported no error — the CSI driver / snapshot-controller is stuck or very slow. |
Check the driver and the snapshot-controller; raise spec.staging.timeout (or set it to "0" to wait indefinitely) if the backend is just slow. |
SourceStaged=False, reason SourceNotCSIProvisioned |
The source PVC has no StorageClass (a static/hostPath volume) — nothing to snapshot. |
Use a CSI-provisioned PVC, or copyMethod: Direct. |
SourceStaged=False, reason StagedClassNotFound |
spec.staging.storageClassName names a StorageClass that doesn't exist. |
Create the class, point the override at an existing one, or remove the override to stage on the source's class. |
SourceStaged=False, reason StagedClassMismatch |
spec.staging.storageClassName is on a different CSI driver than the source — its provisioner can never restore/clone from your source, so the staged PVC would never bind. |
Point the override at a class of the source's driver (the message names both), or remove it. |
SourceStaged=False, reason WaitingForStagedPvcBind (Pending, transient) |
The staged PVC is still binding — the CSI restore/clone from the source is provisioning. Normal for slow restores; bounded by the staging deadline. | Nothing, usually — it either binds or fails at the deadline with the row below. |
SourceStaged=False / phase Failed, reason StagedPvcBindTimeout |
The staged PVC never reached Bound within spec.staging.timeout — the restore/clone is still provisioning (e.g. a CephFS full clone of a small-file-heavy volume) or the class can't provision it. |
kubectl describe pvc <name>-src + the CSI provisioner logs; raise spec.staging.timeout if the copy is just slow, or (CephFS) stage on a backingSnapshot: "true" shallow class. |
Phase Failed, reason StagedPvcLost |
The staged PVC reports Lost — its bound PV disappeared mid-stage. |
Check the CSI driver / PV lifecycle; the next scheduled run retries. |
Backup stuck Pending, staged PVC Pending |
WaitForFirstConsumer (normal — binds when the mover starts) or the driver can't clone (for Clone). |
If it never binds, the backup fails at the staging deadline with StagedPvcBindTimeout; kubectl describe pvc <name>-src for the driver event; switch method if cloning is unsupported. |
See also Troubleshooting → Multi-Attach for the Direct-mode co-location path.