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.

  • 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.

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"
---
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
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).

Customization — the values you actually change

Section titled “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.

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.

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:

Terminal window
# 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.

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

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.

1. Apply the bundle. That is the backups namespace, the Secret, the repository PVC and Repository, and the smoke-test objects:

Terminal window
$ 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:

Terminal window
$ 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:

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

4. Watch it succeed:

Terminal window
$ 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:

Terminal window
$ 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:

Terminal window
$ 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:

Terminal window
$ 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

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.

  • 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.