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. What gets backed up, and
how, is in ../backups/README.md.
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 |
Both allow volume expansion. Defined in
proxmox-csi-plugin/storageclass.yaml
and csi-driver-nfs/storageclass.yaml.
A claim without storageClassName lands on proxmox-local.
Proxmox CSI¶
The chart proxmox-csi-plugin runs in csi-proxmox (sync wave 0), with the controller as a
single replica on the control plane
(values.yaml). It talks to the
Proxmox API on port 8006 as kubernetes-csi@pve with role CSI, both managed in
access.tf.
A volume is an LVM disk named vm-9999-pvc-<uid> on the local-lvm of the host where the
first pod was scheduled, hot-plugged into the Talos VM that pod runs on. 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 tied to one host. Control planes
take workloads (allowSchedulingOnControlPlanes), so the pod can run on either the
control plane or 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", so the disk is included when the Proxmox backup job
backs up the VM it is attached to. The chart's snapshotter sidecar is enabled, but no
VolumeSnapshotClass or snapshot controller exists, so CSI snapshots are 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, set as
existingConfigSecret in the values. 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), one controller replica,
snapshots off, and the StorageClass comes from this repo rather than the chart
(values.yaml). Each claim gets the
directory /volume1/k8s-volumes/talos/<namespace>/<pvc> and can be mounted from any node.
A fresh directory belongs to root and is not writable for other uids. 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).
Static NFS volumes¶
These PVs have storageClassName: "", reclaim Retain and a claimRef to one PVC, so
deleting the claim leaves the data 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 |
The media apps, audiobookshelf and verksted mount /volume1/shared-data (or a folder under
it) as inline nfs volumes in the pod spec instead, and the VolSync movers and
backup-check jobs mount /volume1/k8s-backups the same way. A namespace that enforces Pod Security
restricted refuses inline nfs volumes, so there the NFS path goes through a static PV
and PVC, as reelsmith does.
Choosing a class¶
proxmox-localfor app config and SQLite databases of workloads that can live 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 sits 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>instead of deleting it. Nothing cleans these up; remove them on the NAS once 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 and the PV delete stays stuck until the old copy is removed.- 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 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 lands on
the host of the first pod that mounts the claim, which for a restore is the VolSync mover,
so check the new PV's zone affinity before switching 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; one without a matching
PV volumeHandle is left over from a deleted claim and can be removed with pvesm free.
Known gaps¶
- A Proxmox CSI detach that loses the VM config lock leaves the disk as a pending delete and
breaks that VM's backups;
pbs-backup-checkcatches it. Tracked indocs/backlog/README.md. - The
k8s-volumesshare has no immutable snapshots or data checksumming. Tracked indocs/backlog/README.md.