Kubernetes separates storage provisioning (creating storage volumes) from storage consumption (using volumes in pods) through two resource types: PersistentVolumes (PVs) and PersistentVolumeClaims (PVCs). A PersistentVolume is a piece of storage in the cluster provisioned by an administrator or dynamically by a StorageClass. A PersistentVolumeClaim is a request for storage by a user — it specifies the required capacity and access mode, and Kubernetes binds it to a matching PV. StorageClasses enable dynamic provisioning, where PVs are automatically created when a PVC is submitted, without requiring pre-created PVs. On RHEL 9 bare-metal clusters, the most common local storage solution is the local-path-provisioner (built into k3s) or manual local type PVs using NFS or hostPath for development.
Prerequisites
- Kubernetes cluster running on RHEL 9
Step 1 — Install Local Path Provisioner (for kubeadm clusters)
# k3s includes local-path-provisioner by default
# For kubeadm clusters, install it manually:
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
# Set as the default StorageClass
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
# Verify
kubectl get storageclass
Step 2 — Create a PersistentVolumeClaim
# /tmp/postgres-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: database
spec:
accessModes:
- ReadWriteOnce # Mounted to a single node at a time
resources:
requests:
storage: 20Gi
storageClassName: local-path # Or "standard" for cloud providers
kubectl create namespace database
kubectl apply -f /tmp/postgres-pvc.yaml
kubectl get pvc -n database
Step 3 — Use a PVC in a StatefulSet
# /tmp/postgres-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: database
spec:
selector:
matchLabels:
app: postgres
serviceName: postgres
replicas: 1
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
Step 4 — NFS StorageClass (for shared storage)
# Install NFS server on RHEL 9
dnf install -y nfs-utils
mkdir -p /srv/nfs/k8s
echo '/srv/nfs/k8s *(rw,sync,no_subtree_check,no_root_squash)' >> /etc/exports
systemctl enable --now nfs-server
exportfs -ra
# Install NFS subdir external provisioner via Helm
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner
--set nfs.server=192.168.1.100
--set nfs.path=/srv/nfs/k8s
--set storageClass.name=nfs-client
Conclusion
Kubernetes persistent storage on RHEL 9 requires choosing the right StorageClass for your workload: local-path for single-node development clusters (data is node-local and not replicated), NFS for shared access across multiple pods or nodes, and cloud-provider storage classes for production cloud deployments. StatefulSets are the recommended Kubernetes resource for stateful workloads like databases — they provide stable pod identities, ordered deployment, and per-pod PVC templates that ensure each database replica has its own dedicated persistent volume.
Next steps: How to Deploy Applications to Kubernetes on RHEL 9, How to Set Up Kubernetes Ingress on RHEL 9, and How to Install k3s on RHEL 9.