Skip to content

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 (and whether) a backup or restore can run alongside your app.

Everything on this page is automatic — there is nothing to install or enable. The behavior is driven by moverDefaults.sourceColocation (default Auto); you only touch that knob to opt out.

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

Prove the headline RWOP claim — back up a held ReadWriteOncePod volume with zero downtime — with one self-contained bundle, deploy/examples/tryit/access-modes.yaml: namespace, a filesystem Repository on a PVC, an RWOP app-data PVC, a long-running holder Deployment that mounts it (writing a marker first, then sleeping), a copyMethod: Snapshot policy, and a fixed-name Snapshot.

The two pieces that make the RWOP point: the source PVC is ReadWriteOncePod (exclusive to a single pod cluster-wide)…

# A ReadWriteOncePod source — exclusive to a single pod cluster-wide. CSI-only.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: kopiur-tryit
spec:
  accessModes: ["ReadWriteOncePod"]
  storageClassName: REPLACE_ME # a CSI StorageClass (RWOP requires CSI)
  resources:
    requests:
      storage: 1Gi

…and 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/v1alpha1
kind: SnapshotPolicy
metadata:
  name: app-data
  namespace: kopiur-tryit
spec:
  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: 4

Prerequisite: CSI + the snapshot stack

RWOP is CSI-only, and copyMethod: Snapshot needs the external-snapshotter plus a VolumeSnapshotClass for your driver (a driver new enough for RWOP almost always ships snapshots too). Fill in both REPLACE_ME values: storageClassName (a CSI class) and KOPIA_PASSWORD.

1. Apply and wait — and note 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=2m

2. 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=holder
NAME                      READY   STATUS    RESTARTS   AGE
holder-7d9c8b6f4c-x2k9p   1/1     Running   0          30s

3. 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-src

The backup mounted app-data-snapshot-src (the staged copy), 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=holder
NAME                      READY   STATUS    RESTARTS   AGE
holder-7d9c8b6f4c-x2k9p   1/1     Running   0          6m

Contrast: Direct fails fast on a held RWOP volume

Change the policy to copyMethod: Direct and re-run, and the backup fails immediately — a second pod (the mover) cannot mount an RWOP volume even on the same node — with the actionable message shown in Backing up an RWOP volume with Direct below. That fast-fail is the point: Kopiur won't leave a mover stuck Pending forever.

To tear down: kubectl delete namespace kopiur-tryit.

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. No pinning, no restrictions.

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. Full detail (discovery order, the Required/Disabled modes, RBAC needs) is in Repositories → sourceColocation.

ReadWriteOncePod — exclusive to one pod, so pick the right copy method

ReadWriteOncePod (RWOP, GA since Kubernetes 1.29, 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.

What Kopiur does about it, per situation:

With copyMethod: Snapshot (or Clone), the mover never mounts your live volume. Kopiur takes a CSI VolumeSnapshot (or CSI clone) of the source — a storage-layer operation that the RWOP mount exclusivity does not restrict — 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 — e.g. [ReadOnlyMany] for a snapshot-backed read-only class like 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/access-modes.md.
#
# 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/v1alpha1
kind: SnapshotPolicy
metadata:
  name: postgres-rwop
  namespace: databases
spec:
  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: false

Clone of an in-use volume is driver-dependent

CSI snapshots of attached volumes are universally supported. CSI clones of an attached volume are up to the driver — some refuse and leave the staged PVC Pending. If that happens, prefer copyMethod: Snapshot.

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 — 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 is currently held by a running pod; a second
pod (the backup mover) cannot mount it even on the same node — scale the workload
down before backing it up, switch the PVC to ReadWriteMany, or set
moverDefaults.sourceColocation.mode=Disabled

Your options, in order of preference:

  1. Switch to copyMethod: Snapshot (above) — no downtime, point-in-time, app-decoupled.
  2. Scale the workload down for the backup window (kubectl scale deploy/<app> --replicas=0). With the volume released, Direct works; scale back up afterwards.
  3. Change the PVC's access mode to ReadWriteOnce if you don't actually need single-pod exclusivity — RWO still guarantees single-node attachment, and Kopiur co-locates the mover automatically.

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 (the mover is the sole pod). Restoring into an existing RWOP PVC (target.pvcRef) requires scaling the workload down first — which you generally want during a restore anyway, so the app doesn't read or write data mid-rewrite. A held RWOP target fails fast with the same actionable message as above.

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/tolerations. Use it only when you manage placement and volume hand-offs yourself — e.g. 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 will sit Pending on a mount conflict instead of failing with guidance.

RWOP failures are structural, not transient

A held-RWOP failure is reported as a validation failure (the run's condition and an Event carry the message above) and will recur on every run until you change something — scale the holder down, switch the copy method, or change the access mode. Kopiur won't retry its way out of it. See Troubleshooting.