Skip to content

Filesystem (PVC or inline NFS)

The filesystem backend stores the kopia repository on a local path that Kopiur mounts into the mover. Behind that path is either a PersistentVolumeClaim or an inline NFS export, which is volume.nfs and needs no PVC. Either way it is typically a NAS or NFS share. See Inline NFS below.

There are no object-store credentials here. The only secret is KOPIA_PASSWORD. What bites people on this backend is ownership, not authentication.

Reach for this when your off-site copy 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, which an NFS or NAS StorageClass provides. Backup, restore, and maintenance movers may run as different Jobs at the same time, and they all need to 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, which defaults to 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. This is 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 it entirely only if path already exists on the node or image, as a hostPath.
volume.pvc.name nas-repo The PersistentVolumeClaim mounted read-write at path. It must be ReadWriteMany, because 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 or absent volume means "the path is already present in the mover", such as a hostPath or a mount baked into the image. That is mainly used by the e2e harness.

Customization — the values you actually change

  • volume.pvc.name names the PVC to mount. Its size and StorageClass live on the PVC, not here.
  • volume.nfs points straight at an NFS export instead of a PVC. See Inline NFS below.
  • path is the in-pod mount point. /repo is a fine default.
  • The mover securityContext is where you set runAsUser and fsGroup on the consuming SnapshotPolicy, to match the share's ownership. See Permissions.
  • create.enabled initializes the repository if it's missing.

Sizing the PVC

The bundled example requests 500Gi as a placeholder. Size yours to the deduplicated, compressed repository, not to 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 to 1.5 times the source data. Watch actual usage after the first retention cycle and resize; most NAS-backed StorageClasses support volume expansion.

The failure mode to avoid is running the volume completely full, because 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 the classic root_squash setting on the export doesn't matter here. 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

Your NAS may force all clients to one identity, through all_squash or a "map all users" setting. In that case point the mapping, meaning anonuid and anongid, at the directory owner. 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

The natural instinct is to set moverDefaults.podSecurityContext.fsGroup to the export's GID. That does nothing: fsGroup has no effect on NFS, because the kubelet doesn't chown in-tree NFS mounts.

Suppose the export is owned by a dedicated UID and GID while your apps run as other UIDs. Use a shared supplemental group instead. Make the export group-writable with chown root:3001 … && chmod 2775 …, then give that group to everything that writes to the backend, meaning the movers and the kopia-ui server:

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

Per-policy source reads stay correct, because the mover reads the source as the app's UID and the group is additive.

The admission webhook warns when an NFS filesystem repository relies only on fsGroup. For the full recipe, see Security context → NFS filesystem repositories.

Inline NFS (no PVC)

kopia has no native NFS backend. You reach NFS 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. The operator then builds a Kubernetes inline nfs volume on every mover Job: bootstrap, backup, restore, and 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 repository is reachable from the controller, so it is connected or created in-process.

A PVC-backed or NFS-backed repository is not reachable from the controller. So the operator runs the connect-or-create in a short mover Job that mounts the volume, which is the same route object stores take. The Repository moves from Initializing to Ready as that Job completes.

As a ClusterRepository

A ClusterRepository may also use a filesystem backend.

For a PVC, the claim must exist, and be ReadWriteMany, in whatever namespace the movers run in. See Movers.

An inline NFS export is reachable from any mover namespace, because it is named rather than claimed. That can make it simpler than a cross-namespace PVC, though a cloud or 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, and a Snapshot, all pointed at the nas-primary repository above. It takes you from "applied" to "a snapshot on my NAS" in one go.

Two prerequisites for filesystem/NFS

  • The repository volume needs a ReadWriteMany 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 or path must be writable by the mover UID 65532. fsGroup has no effect on NFS. Run chown -R 65532:65532 on 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. That is the backups namespace, the Secret, the repository PVC and Repository, and the smoke-test objects:

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

2. Wait for the repository to be Ready. Everything else waits on this. A volume-backed repository bootstraps in a short mover Job, as the note above explains, so this can take a little longer than an object-store repository:

$ 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 rather than apply 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

The output above is illustrative. The Snapshot has no fixed Succeeded condition, so 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. Prove the data really moved. status.stats shows non-zero bytesNew and 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 are illustrative; sizes and the ID vary. Non-zero bytesNew proves the backup wrote real content to the repository path.

6. Clean up the smoke-test when you're done. This leaves the repository PVC in place; 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 through a finalizer. Use Retain or Orphan to keep the data. See Backups → deletionPolicy.

From here the rest of the lifecycle is the same on every backend. Only the Repository differs. Put it on a cron with a SnapshotSchedule, described in Backups & schedules and Example 01. Restore by picking a Snapshot, described in Restores and 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, so it blocks the others. Use ReadWriteMany.

An inline NFS export sidesteps this, because NFS is multi-mount by nature and 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, which defaults to 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 and GID to the share owner through the mover securityContext. For the full story, see Permissions, UID & GID.

  • permission denied on create or connect. Run chown -R 65532 <path>, or set the mover UID to the owner. If the export is owned by a dedicated UID and GID while 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 and path are reachable from the cluster nodes and that the export permits them.

See also