Skip to content

Storage

Two CSI drivers provide dynamic volumes: Proxmox CSI for block disks on each host's local-lvm, and csi-driver-nfs for directories on the Synology share k8s-volumes. A few static NFS PersistentVolumes point at fixed folders on the NAS. ../backups/README.md lists what is backed up and how.

A claim routed through proxmox-local, syno-nfs-csi or a static PV to a Proxmox local-lvm disk or a folder on the NAS

Storage classes

Class Driver Backing store Binding Reclaim Default
proxmox-local csi.proxmox.sinextra.dev local-lvm on the Proxmox host that runs the pod, ext4, SSD, writethrough cache WaitForFirstConsumer Delete yes
syno-nfs-csi nfs.csi.k8s.io nas.local.bigd.no:/volume1/k8s-volumes/talos, one subdirectory <namespace>/<pvc> per claim, onDelete: archive Immediate Delete no
syno-nfs-csi-scratch nfs.csi.k8s.io same share and layout, onDelete: delete; for the restore tests' throwaway volumes only Immediate Delete no

Both allow volume expansion. They are defined in proxmox-csi-plugin/storageclass.yaml and csi-driver-nfs/storageclass.yaml. A claim without storageClassName goes to proxmox-local.

Proxmox CSI

The chart proxmox-csi-plugin runs in csi-proxmox (sync wave 0). The controller runs as a single replica on the control plane (values.yaml). It calls the Proxmox API on port 8006 as kubernetes-csi@pve with the role CSI. access.tf manages both.

A volume is an LVM disk named vm-9999-pvc-<uid> on the local-lvm of the host where the first pod was scheduled. It is hot-plugged into the Talos VM that runs the pod. The PV carries a nodeAffinity on topology.kubernetes.io/region (the Proxmox cluster name) and topology.kubernetes.io/zone (the host, hyper1 to hyper3). Talos sets both labels on every node from talos-cluster.tf. The volume and every pod that mounts it are therefore bound to one host. Control planes accept workloads (allowSchedulingOnControlPlanes), so the pod can run on the control plane or on the worker VM of that host. Several apps also pin themselves to hyper1 with a zone affinity in their Deployment (for example sonarr.yaml).

The StorageClass sets backup: "true". The Proxmox backup job therefore includes the disk when it backs up the VM that the disk is attached to. The chart's snapshotter sidecar is enabled, but no VolumeSnapshotClass or snapshot controller exists. CSI snapshots are therefore not used.

Config secret

The plugin reads its cluster config (Proxmox API endpoint, the kubernetes-csi@pve token, the region name) from the Secret proxmox-csi-plugin, key config.yaml. The values set it as existingConfigSecret. The ExternalSecret in config-secret.yaml builds it from one Bitwarden item through the bitwarden-secretsmanager ClusterSecretStore, refreshed hourly, with deletionPolicy: Retain. To rotate the token, edit the whole config.yaml in that Bitwarden item. The pattern is in ../secrets/README.md.

Synology NFS

The chart csi-driver-nfs runs in csi-driver-nfs (sync wave 2), with one controller replica and snapshots off. The StorageClass comes from this repo, not from the chart (values.yaml). Each claim gets the directory /volume1/k8s-volumes/talos/<namespace>/<pvc>. Any node can mount it.

A new directory belongs to root, and other uids cannot write to it. A container that runs as non-root and creates directories in its volume needs an initContainer that chowns the mount root first. The logeverylift Postgres is the example (../data/postgres.md). VolSync movers write as root for the same reason (../backups/README.md).

To move data that a root container wrote to a non-root uid, run a chown -R and an explicit chmod on every entry (directories 0775, files 0664), once, from a root pod. The NAS keeps an ACL that admits only root until the mode is set. A chown alone therefore still leaves the new owner with Permission denied.

Static NFS volumes

These PVs have storageClassName: "", reclaim Retain and a claimRef to one PVC. When the claim is deleted, the data stays on the NAS. The target folder must exist before the pod starts.

PV Namespace NAS path Defined in
etcd-backup etcd-backup /volume1/k8s-backups/etcd storage.yaml
logeverylift-db-backup logeverylift /volume1/k8s-backups/postgres/logeverylift db-backup.yaml
authentik-db-backup identity /volume1/k8s-backups/postgres/authentik db-backup.yaml
reelsmith-offsite-backups reelsmith /volume1/shared-data/media/reelsmith offsite-pv.yaml
arr-stack-media arr-stack /volume1/shared-data/media nfs-volumes.yaml
plex-media-stack-media plex-media-stack /volume1/shared-data/media (read-only, one subPath per library) nfs-volumes.yaml
gluetun-vpn-torrents gluetun-vpn /volume1/shared-data/media/torrents nfs-volumes.yaml
audiobookshelf-shared-data audiobookshelf /volume1/shared-data nfs-volumes.yaml
verksted-shared-data verksted /volume1/shared-data nfs-volumes.yaml
backup-check-k8s-backups backup-check /volume1/k8s-backups (read-only) nfs-volumes.yaml

Workloads never mount NFS inline. The restricted Pod Security standard refuses inline nfs volumes, so every NAS path goes through a static PV and PVC. The VolSync movers are the exception, because VolSync builds their pod spec.

Choosing a class

  • proxmox-local for app config and SQLite databases of workloads that can run on one host: the arr-stack, qbittorrent and prowlarr, Plex and Tautulli, audiobookshelf, trek, Loki and Tempo.
  • syno-nfs-csi for data that should follow the pod to any node: the logeverylift Postgres, Prometheus, Alertmanager and Grafana, mealie, open-webui, headroom, verksted, reelsmith, seerr, the Ollama model cache, and the VolSync cache volumes.
  • A static PV when the data must outlive the claim or is at a fixed NAS path.

A claim's class cannot change after it is created. The Authentik chart values ask for syno-nfs-csi, but the existing claim data-authentik-postgresql-0 is on proxmox-local and stays there until the claim is recreated.

Deleting a PVC

  • proxmox-local: the PV is deleted, and the plugin removes the LVM disk from the host.
  • syno-nfs-csi: the PV is deleted, and the driver renames the directory to /volume1/k8s-volumes/talos/archived-<namespace>/<pvc>. It does not delete it. Nothing removes these directories. Remove them on the NAS when the data is no longer needed. A new claim with the same name starts empty. If an archived copy of the same claim already exists, the rename fails. The PV delete then stays blocked until the old copy is removed.
  • syno-nfs-csi-scratch: the PV and its directory are deleted. Only the restore tests use it, because they recreate the same claim name every month.
  • Static PVs: the PV goes to Released and keeps the data. To bind a new claim, clear the PV's claimRef.uid or delete and recreate the PV.

Moving a Proxmox volume to another host

Nothing in the repo migrates a proxmox-local disk between hosts. The PV's zone affinity keeps it on the host where it was created. The route the repo supports is a VolSync restore into a new claim: scale the app to zero, create the new PVC, restore the latest backup into it as in ../backups/restore.md, change the Deployment's zone pin where it has one, point the Deployment at the new claim, and delete the old one. The disk is created on the host of the first pod that mounts the claim. For a restore, that pod is the VolSync mover. Check the new PV's zone affinity before you switch the Deployment over.

Checking it

kubectl get storageclass
kubectl get pv -o custom-columns=NAME:.metadata.name,CLASS:.spec.storageClassName,RECLAIM:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name,STATUS:.status.phase
kubectl -n csi-proxmox logs deploy/proxmox-csi-plugin-controller -c proxmox-csi-plugin-controller
kubectl -n csi-driver-nfs get pods

pvesm list local-lvm on a host lists its vm-9999-pvc-* disks. A disk without a matching PV volumeHandle is left over from a deleted claim. pvesm free removes it.

Known gaps

  • A Proxmox CSI detach that loses the VM config lock leaves the disk as a pending delete. This breaks that VM's backups. pbs-backup-check detects it. Tracked in docs/backlog/README.md.
  • The k8s-volumes share has no immutable snapshots or data checksumming. Tracked in docs/backlog/README.md.