Skip to content

Filesystem (PVC or inline NFS)

The filesystem backend stores the kopia repository on a local path Kopiur mounts into the mover. That path is backed by either a PersistentVolumeClaim or an inline NFS export (volume.nfs — no PVC; see below), typically a NAS/NFS share. There are no object-store credentials: the only secret is KOPIA_PASSWORD. The thing that bites people here is ownership, not auth.

Reach for this when your "off-site" is an on-prem NAS or any ReadWriteMany volume. For a remote server reached over SSH, see SFTP.

Provider prerequisites

  • Storage the mover can mount read-write: either a PersistentVolumeClaim or an NFS export. For a PVC, use ReadWriteMany (NFS/NAS) so that backup, restore, and maintenance movers — which may run as different Jobs at the same time — can all mount it. The example bundles a PVC; the inline-NFS variant needs no PVC at all.
  • The repository path must be writable by the UID the mover runs as (default 65532). See Troubleshooting.

The Secret shape

Filesystem backends need only the repository encryption password.

Secret key Required What it is
KOPIA_PASSWORD yes The repository encryption password. No backend auth keys.
stringData:
    KOPIA_PASSWORD: "choose-something-long-and-random"

Lose the password, lose the backups

Even though the data sits on your own NAS, kopia still encrypts it with KOPIA_PASSWORD. Lose the password and the repository is unrecoverable. Store it outside the cluster and back up the Secret. See Encryption.

The Repository

---
apiVersion: v1
kind: Secret
metadata:
  name: fs-repo-creds
  namespace: backups
type: Opaque
stringData:
  # Filesystem backends need ONLY the repository encryption password.
  KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nas-repo
  namespace: backups
spec:
  accessModes: ["ReadWriteMany"] # the mover Job mounts this read-write
  resources:
    requests:
      storage: 500Gi
  # storageClassName: your-nfs-or-nas-class
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
  name: nas-primary
  namespace: backups
spec:
  backend:
    filesystem:
      path: /repo # mount path INSIDE the mover pod
      # `volume` is what backs `path`: a PVC (here) or an inline NFS export
      # (see nfs.yaml). Omit `volume` entirely if `path` already exists on the
      # node/image (a hostPath/baked-in mount).
      volume:
        pvc:
          name: nas-repo # the PVC mounted read-write at `path`
  encryption:
    passwordSecretRef:
      name: fs-repo-creds
      key: KOPIA_PASSWORD
  create:
    enabled: true

Fields reference (backend.filesystem)

Field Required Default Example What it controls
path yes /repo Mount path inside the mover pod where kopia writes the repository. Not a path on your NAS — the volume decides what's behind it.
volume no What backs path: exactly one of pvc or nfs. Omit entirely only if path already exists on the node/image (hostPath).
volume.pvc.name nas-repo The PersistentVolumeClaim mounted read-write at path. Must be ReadWriteMany (movers overlap).
volume.nfs.server nas.lan NFS server hostname or IP — for an inline NFS export with no PVC (see below).
volume.nfs.path /export/kopia The absolute export path on the NFS server (what showmount -e nas.lan lists).

volume is an "exactly one of" choice

volume: { pvc: … } and volume: { nfs: … } are externally-tagged variants — set one, never both. An empty/absent volume means "the path is already present in the mover" (a hostPath/baked-in mount; mainly the e2e harness).

Customization — the values you actually change

  • volume.pvc.name — the PVC to mount (its size/StorageClass live on the PVC, not here).
  • volume.nfs — point straight at an NFS export instead of a PVC (below).
  • path — the in-pod mount point; /repo is a fine default.
  • mover securityContext — set runAsUser/fsGroup on the consuming SnapshotPolicy to match the share's ownership; see Permissions.
  • create.enabled — initialize the repository if missing.

Sizing the PVC

The bundled example requests 500Gi as a placeholder — size yours to the deduplicated, compressed repository, not the raw source data. kopia content-addresses everything, so N daily snapshots of slowly-changing data cost roughly one full copy plus the churn, not N copies. A reasonable starting point is 1–1.5× the source data; watch actual usage after the first retention cycle and resize (most NAS-backed StorageClasses support volume expansion). Running the volume completely full is the failure mode to avoid — kopia maintenance needs headroom to rewrite and compact blobs.

Preparing the export (NFS-side ownership)

The mover runs as UID 65532 by default and is not root, so classic root_squash on the export is irrelevant — what matters is that UID 65532 can write the directory:

# on the NAS / NFS server
$ mkdir -p /export/kopia
$ chown -R 65532:65532 /export/kopia

If your NAS forces all clients to one identity (all_squash / "map all users"), point that mapping (anonuid/anongid) at the directory owner instead — or set the mover's runAsUser to whatever UID the NAS expects. The full decision table is in Permissions, UID & GID.

fsGroup does not work here

A natural instinct is to set moverDefaults.podSecurityContext.fsGroup to the export's GID — but fsGroup is a no-op on NFS (the kubelet doesn't chown in-tree NFS mounts). For an export owned by a dedicated UID/GID while your apps run as other UIDs, use a shared supplemental group instead: make the export group-writable (chown root:3001 … && chmod 2775 …) and give every backend-writer (movers and the kopia-ui server) that group:

spec:
  moverDefaults:
    podSecurityContext:
      supplementalGroups: [3001]
  server: # only if the web UI is enabled
    podSecurityContext:
      supplementalGroups: [3001]

Per-policy source reads stay correct (the mover reads the source as the app's UID; the group is additive). The admission webhook warns when an NFS filesystem repo relies only on fsGroup. Full recipe: Security context → NFS filesystem repositories.

Inline NFS (no PVC)

kopia has no native NFS backend — NFS is reached through the filesystem backend by mounting the export at path. Instead of pre-creating a ReadWriteMany PVC, name an NFS export directly under volume.nfs and the operator synthesizes a Kubernetes inline nfs volume on every mover Job (bootstrap, backup, restore, maintenance):

---
apiVersion: v1
kind: Secret
metadata:
  name: nfs-repo-creds
  namespace: backups
type: Opaque
stringData:
  # Filesystem/NFS backends need ONLY the repository encryption password — the
  # data sits on your NAS but kopia still encrypts it. No object-store keys.
  KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
  name: nas-primary
  namespace: backups
spec:
  backend:
    filesystem:
      path: /repo # mount path INSIDE the mover pod where kopia writes the repo
      volume:
        nfs:
          server: nas.lan # NFS server hostname or IP
          path: /export/kopia # the export on the server (must be an absolute path)
  encryption:
    passwordSecretRef:
      name: nfs-repo-creds
      key: KOPIA_PASSWORD
  create:
    enabled: true

This is the lowest-friction path to an on-prem NAS repository: no PVC, no StorageClass, no provisioner — just a server and an absolute path. The same volume.nfs shape works on a ClusterRepository. To back up an NFS export as a source (rather than as the repository), see Example 10.

A volume-backed repo bootstraps in a mover Job

A bare-path filesystem repo is reachable from the controller and is connected/created in-process. A PVC- or NFS-backed repo is not reachable from the controller, so the operator runs the connect/create in a short mover Job that mounts the volume — the same path object stores use. The Repository moves InitializingReady as that Job completes.

As a ClusterRepository

A ClusterRepository may also use a filesystem backend. For a PVC, the claim and its ReadWriteMany reach must be available in whatever namespace the movers run in — see Movers. An inline NFS export sees the same reach from any mover namespace (it's named, not claimed), which can make it simpler than a cross-namespace PVC — though a cloud/object backend is usually the better fit for a shared platform repository.

Try it end-to-end

Prove this backend really takes a backup. The same example file carries a tiny smoke-test (a throwaway PVC + a SnapshotPolicy + a Snapshot) that targets the nas-primary repository above, so you can go from "applied" to "a snapshot on my NAS" in one arc.

Two prerequisites for filesystem/NFS

  • The repository volume needs an RWX StorageClass. The bundled nas-repo PVC asks for ReadWriteMany so backup, restore, and maintenance movers can overlap. On a single-node test cluster you can substitute ReadWriteOnce.
  • The export/path must be writable by the mover UID 65532. fsGroup is a no-op on NFS — chown -R 65532:65532 the path on the NAS, or use the shared supplemental group recipe in Preparing the export. Without this the Repository stops at Failed with a permission-denied event.

1. Apply the bundle (namespace backups, Secret, the repo PVC + Repository, and the smoke-test objects):

$ kubectl apply -f deploy/examples/backends/filesystem.yaml

2. Wait for the repository to be Ready — the gate everything else waits on. A volume-backed repo bootstraps in a short mover Job (see the note above), so this can take a touch longer than an object-store repo:

$ kubectl -n backups wait --for=condition=Ready repository/nas-primary --timeout=2m
repository.kopiur.home-operations.com/nas-primary condition met

3. Take the smoke backup. The Snapshot uses generateName, so create it (the namespace, Secret, Repository, PVCs, and policy already exist and report unchanged — the Snapshot is the one new object):

$ kubectl create -f deploy/examples/backends/filesystem.yaml
snapshot.kopiur.home-operations.com/smoke-now-abc12 created

4. Watch it succeed:

$ kubectl -n backups get snapshots -w
NAME              PHASE       ORIGIN   SNAPSHOT     AGE
smoke-now-abc12   Pending     manual                2s
smoke-now-abc12   Running     manual                7s
smoke-now-abc12   Succeeded   manual   k1f1ec0a8    38s

(Output illustrative.) The Snapshot has no fixed Succeeded condition; to wait on it in a script, key on the phase:

$ kubectl -n backups wait --for=jsonpath='{.status.phase}'=Succeeded \
    snapshot/smoke-now-abc12 --timeout=5m

5. Deep proof — the data really moved. status.stats shows non-zero bytesNew/filesNew, and status.snapshot.kopiaSnapshotID is the kopia snapshot ID on your NAS:

$ kubectl -n backups get snapshot smoke-now-abc12 -o jsonpath='{.status.stats}'
{"sizeBytes":4096,"bytesNew":1280,"filesNew":2,"filesUnchanged":0}

$ kubectl -n backups get snapshot smoke-now-abc12 -o jsonpath='{.status.snapshot.kopiaSnapshotID}'
k1f1ec0a8

(Both outputs illustrative — sizes and the ID vary.) Non-zero bytesNew is the proof the backup wrote real content to the repository path.

6. Clean up the smoke-test when you're done (this leaves the repo PVC; delete nas-repo too if you want the repository gone):

$ kubectl -n backups delete snapshot --all       # finalizer also deletes the kopia snapshot
$ kubectl -n backups delete snapshotpolicy smoke
$ kubectl -n backups delete pvc smoke-data

Deleting a Snapshot deletes its snapshot

A produced Snapshot defaults to deletionPolicy: Delete, so removing the CR runs kopia snapshot delete via a finalizer. Use Retain (or Orphan) to keep the data — see Backups → deletionPolicy.

From here the full lifecycle is backend-independent — only the Repository differs. Put it on a cron with a SnapshotSchedule (Backups & schedules, Example 01) and restore by picking a Snapshot (Restores, Example 03).

ReadWriteMany matters (for PVCs)

Backup, restore, and maintenance run as separate mover Jobs and may overlap. A ReadWriteOnce volume can only attach to one node at a time and will block the others; use ReadWriteMany. An inline NFS export sidesteps this — NFS is inherently multi-mount, so concurrent movers all reach it.

Troubleshooting

It's ownership, not a credential

The most common filesystem failure is permission denied on the repository path — the path isn't writable by the mover's UID (default 65532). Kopiur's Warning Event names the exact UID and the chown -R <uid> <path> to run on the NAS. Either chown the path or match the mover's UID/GID to the share owner via the mover securityContext. Full story: Permissions, UID & GID.

  • permission denied on create/connect — chown -R 65532 <path> (or set the mover UID to the owner). If the export is owned by a dedicated UID/GID and your apps run as other UIDs, use a shared supplemental group rather than fsGroup (which NFS ignores). See above.
  • Mover Job pending — for a PVC, it isn't bound or isn't ReadWriteMany; check the PVC and StorageClass. For NFS, the pod can't mount the export — confirm the server/path are reachable from the cluster nodes and the export permits them.

See also