PVC access modes (RWO, RWX, RWOP)
Kopiur’s movers are ordinary pods. A backup mover must mount the volume it reads, and a restore mover must mount the volume it writes.
Whether Kubernetes lets a second pod do that while your application is running is governed by the PVC’s access mode. So the access mode, together with the copy method, decides how a backup or restore can run alongside your app, and whether it can run at all.
Everything on this page is automatic. There is nothing to install or enable. The behavior comes from moverDefaults.sourceColocation, which defaults to Auto, and you only touch that knob to opt out.
Compatibility at a glance
Section titled “Compatibility at a glance”| Access mode | Direct backup (mounts the live PVC) |
Snapshot / Clone backup (mounts a staged copy) |
Restore into the PVC |
|---|---|---|---|
ReadWriteMany / ReadOnlyMany |
✅ mover schedules freely | ✅ | ✅ |
ReadWriteOnce (RWO) |
✅ mover co-locates onto the attach node automatically | ✅ | ✅ co-locates automatically |
ReadWriteOncePod (RWOP) |
⚠️ only while no pod holds the volume; a held volume fails fast with guidance | ✅ works with no downtime, recommended | ⚠️ only while no pod holds the volume |
Try it end-to-end
Section titled “Try it end-to-end”Prove the headline RWOP claim, which is that you can back up a held ReadWriteOncePod volume with zero downtime.
It is one self-contained bundle, deploy/examples/tryit/access-modes.yaml. It contains a namespace, a filesystem Repository on a PVC, an RWOP app-data PVC, a long-running holder Deployment that mounts it and writes a marker before sleeping, a copyMethod: Snapshot policy, and a fixed-name Snapshot.
Two pieces make the point. First, the source PVC is ReadWriteOncePod, so it is exclusive to a single pod cluster-wide:
# A ReadWriteOncePod source — exclusive to a single pod cluster-wide. CSI-only.apiVersion: v1kind: PersistentVolumeClaimmetadata: name: app-data namespace: kopiur-tryitspec: accessModes: ["ReadWriteOncePod"] storageClassName: REPLACE_ME # a CSI StorageClass (RWOP requires CSI) resources: requests: storage: 1GiSecond, the policy uses copyMethod: Snapshot, so the mover reads a staged copy and never contends for that single-pod mount:
apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotPolicymetadata: name: app-data namespace: kopiur-tryitspec: repository: name: primary # Snapshot (not Direct) is what lets the backup run while `holder` keeps the # RWOP volume mounted: the mover reads a staged copy, never the live volume. copyMethod: Snapshot sources: - pvc: name: app-data retention: keepDaily: 7 keepWeekly: 41. Apply and wait. Note that you do not scale the holder down.
$ kubectl apply -f deploy/examples/tryit/access-modes.yaml$ kubectl -n kopiur-tryit rollout status deploy/holder --timeout=2m$ kubectl -n kopiur-tryit wait --for=condition=Ready repository/primary --timeout=2m2. Confirm the holder owns the live volume before the backup. It should be Running with 0 restarts:
$ kubectl -n kopiur-tryit get pods -l app=holderNAME READY STATUS RESTARTS AGEholder-7d9c8b6f4c-x2k9p 1/1 Running 0 30s3. Back it up without touching the holder, and read status.staged (deep). The mover reads a staged copy, so the RWOP exclusivity is never violated:
$ 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.pvcName}'app-data-snapshot-srcThe backup mounted app-data-snapshot-src, the staged copy, and never the live app-data.
4. Confirm the holder never flinched. Same pod, still Running, still 0 restarts:
$ kubectl -n kopiur-tryit get pods -l app=holderNAME READY STATUS RESTARTS AGEholder-7d9c8b6f4c-x2k9p 1/1 Running 0 6mTo tear down, run kubectl delete namespace kopiur-tryit.
ReadWriteMany / ReadOnlyMany — nothing to think about
Section titled “ReadWriteMany / ReadOnlyMany — nothing to think about”The volume can be attached to many nodes and mounted by many pods at once. The mover schedules wherever the cluster likes, alongside your running app. There is no pinning and no restriction.
ReadWriteOnce — handled automatically
Section titled “ReadWriteOnce — handled automatically”An RWO volume attaches to one node at a time, but any number of pods on that node may mount it.
Kopiur detects the node your app holds the volume on and pins the mover there, so backups and restores of in-use RWO PVCs just work. This is the default sourceColocation.mode: Auto behavior, and it avoids the Kubernetes Multi-Attach error. For the full detail, meaning the discovery order, the Required and Disabled modes, and the RBAC it needs, see Repositories → sourceColocation.
ReadWriteOncePod — exclusive to one pod, so pick the right copy method
Section titled “ReadWriteOncePod — exclusive to one pod, so pick the right copy method”ReadWriteOncePod (RWOP), which has been GA since Kubernetes 1.29 and works on CSI volumes only, hardens RWO’s guarantee: the volume can be mounted by a single pod cluster-wide.
That single-pod exclusivity is exactly what makes it attractive for databases, and exactly what a backup tool has to plan around, because the mover is a second pod. Unlike RWO, co-locating the mover on the same node cannot help: the kubelet refuses the second mount even there.
Here is what Kopiur does about it, per situation.
Backing up an RWOP volume with Snapshot or Clone — no downtime (recommended)
Section titled “Backing up an RWOP volume with Snapshot or Clone — no downtime (recommended)”With copyMethod: Snapshot or Clone, the mover never mounts your live volume.
Kopiur takes a CSI VolumeSnapshot, or a CSI clone, of the source. That is a storage-layer operation, which the RWOP mount exclusivity does not restrict. It then provisions a temporary staged PVC from it and runs kopia against that stage.
By default the staged PVC inherits your source’s access modes, RWOP included, but the mover is its only pod, so the exclusivity is satisfied. Your app keeps running, untouched. spec.staging.accessModes can override the staged PVC’s modes, for example to [ReadOnlyMany] for a snapshot-backed read-only class such as CephFS backingSnapshot; see Copy methods → staging overrides.
This is the recommended way to back up RWOP volumes, and you almost certainly already have what it needs: RWOP itself requires a CSI driver, and most CSI drivers that ship RWOP support also ship snapshots.
# Example 24 — Back up a ReadWriteOncePod (RWOP) PVC with no downtime## A ReadWriteOncePod PVC can be mounted by exactly ONE pod cluster-wide, so# copyMethod: Direct cannot mount it while your app holds it — a held RWOP volume# fails fast with guidance instead of leaving a mover stuck Pending. copyMethod:# Snapshot (the default; shown explicitly here for clarity) sidesteps the whole# problem: Kopiur snapshots the volume at the STORAGE layer (which RWOP's mount# exclusivity does not restrict), stages a temporary PVC from the snapshot, and the# mover reads that stage — never the live volume. The staged PVC inherits RWOP, but# the mover is its only pod, so the exclusivity is satisfied. Your app keeps# running, untouched. See docs/src/content/docs/access-modes.mdx.## REQUIRES the CSI snapshot stack (external-snapshotter + CRDs) and a# VolumeSnapshotClass for your driver — and since RWOP itself is CSI-only, a driver# new enough for RWOP almost always ships snapshot support too.## The source PVC referenced below is your application's existing claim, e.g.:# spec:# accessModes: ["ReadWriteOncePod"] # single pod cluster-wide# storageClassName: csi-rbd # any CSI class; RWOP requires CSI## Field shapes verified against crates/api.---apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotPolicymetadata: name: postgres-rwop namespace: databasesspec: repository: name: primary # The one load-bearing line: never mount the live RWOP volume — snapshot it. # (Clone also avoids the live mount, but cloning an ATTACHED volume is # driver-dependent; Snapshot is the safe choice.) copyMethod: Snapshot volumeSnapshotClassName: csi-rbd-snapclass # optional; omit to use the driver default sources: - pvc: name: postgres-data # the app's ReadWriteOncePod PVC retention: keepDaily: 14 keepWeekly: 8 # The snapshot is block-level point-in-time (crash-consistent). For application # consistency, quiesce the database first — the VolumeSnapshot is captured AFTER # beforeSnapshot hooks run. hooks: beforeSnapshot: - workloadExec: podSelector: matchLabels: app.kubernetes.io/name: postgres command: ["/bin/sh", "-c", "psql -c 'CHECKPOINT;'"] continueOnFailure: falseBacking up an RWOP volume with Direct — only while nothing holds it
Section titled “Backing up an RWOP volume with Direct — only while nothing holds it”copyMethod: Direct mounts the live PVC into the mover, so it can only work when no pod currently holds the volume. Then the mover is the sole pod, which RWOP permits, and Kopiur schedules it freely.
If a running pod does hold the volume, Kopiur does not leave a mover stuck Pending forever. The backup fails immediately with an actionable message:
PVC `ns/data` is ReadWriteOncePod and held by a running pod, so the mover cannotmount it. Fix: scale the workload down before backing it up, switch the PVC toReadWriteMany, or set moverDefaults.sourceColocation.mode=DisabledYour options, best first:
- Switch to
copyMethod: Snapshot, described above. No downtime, point-in-time, and decoupled from the app. - Scale the workload down for the backup window, with
kubectl scale deploy/<app> --replicas=0. With the volume released,Directworks. Scale back up afterwards. - Change the PVC’s access mode to
ReadWriteOnceif you do not actually need single-pod exclusivity. RWO still guarantees single-node attachment, and Kopiur co-locates the mover automatically.
Restoring into an RWOP volume
Section titled “Restoring into an RWOP volume”A restore mover writes into the target PVC, so the same rule applies: the target must not be held by a running pod.
Restoring into a freshly created target.pvc always works, because the mover is the sole pod. Restoring into an existing RWOP PVC, meaning target.pvcRef, requires scaling the workload down first, which you generally want during a restore anyway so the app does not read or write data mid-rewrite. A held RWOP target fails fast with the same actionable message as above.
Escape hatch: sourceColocation.mode: Disabled
Section titled “Escape hatch: sourceColocation.mode: Disabled”Setting moverDefaults.sourceColocation.mode: Disabled skips the access-mode checks entirely, along with RWO node pinning, and schedules the mover with only your explicit nodeSelector, affinity and tolerations.
Use it only when you manage placement and volume hand-offs yourself, for example with an external system that releases the volume right before the backup window. If a pod still holds the RWOP volume when the mover starts, the mover pod sits Pending on a mount conflict instead of failing with guidance.