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.
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-localfor 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-csifor 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
Releasedand keeps the data. To bind a new claim, clear the PV'sclaimRef.uidor 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-checkdetects it. Tracked indocs/backlog/README.md. - The
k8s-volumesshare has no immutable snapshots or data checksumming. Tracked indocs/backlog/README.md.