Stateful applications running in Kubernetes — databases, content management systems, message queues — need storage that persists beyond the lifecycle of any individual pod. Kubernetes solves this through two complementary abstractions: PersistentVolumes (PV), which represent actual storage capacity provisioned by an administrator or a storage provider, and PersistentVolumeClaims (PVC), which are requests for storage made by application manifests. On RHEL 8 you can use local host paths for development and testing, and StorageClasses backed by a dynamic provisioner for automated volume management in production. This tutorial covers all three layers, including mounting a PVC inside a running Deployment.
Prerequisites
- A running Kubernetes cluster on RHEL 8 (kubeadm or k3s)
kubectlconfigured and pointing at the cluster- For the StorageClass section: k3s with the bundled
local-pathprovisioner, or the standalone Rancher local-path-provisioner deployed on a kubeadm cluster - Root access to create host directories for PV backing storage
Step 1 — Understand PV and PVC Concepts
A PersistentVolume is a cluster-level resource that abstracts physical storage (NFS, iSCSI, cloud disk, or local disk). A PersistentVolumeClaim is a namespaced request that binds to a compatible PV. Once bound, the PVC can be referenced in a pod’s volumes section. The access mode (ReadWriteOnce, ReadOnlyMany, ReadWriteMany) and the storage capacity are the primary binding criteria. Create the backing directory on the host before creating the PV manifest.
# Create the directory that will back the hostPath PV
mkdir -p /mnt/k8s-data/pv001
chmod 777 /mnt/k8s-data/pv001
Step 2 — Create a PersistentVolume
The manifest below defines a 5 Gi hostPath PersistentVolume. hostPath is suitable for single-node development clusters; multi-node clusters should use NFS or a CSI driver instead.
cat < pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv001
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/k8s-data/pv001
EOF
kubectl apply -f pv.yaml
# Confirm the PV is Available
kubectl get pv pv001
Step 3 — Create a PersistentVolumeClaim
The PVC requests 1 Gi of storage with ReadWriteOnce access. Kubernetes will bind it to the first available PV that satisfies both the access mode and the capacity request.
cat < pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
EOF
kubectl apply -f pvc.yaml
# The STATUS column should show "Bound"
kubectl get pvc my-pvc
kubectl get pv pv001 # STATUS should now show "Bound"
Step 4 — Mount the PVC Inside a Deployment
Reference the PVC in a Deployment by adding a volumes entry at the pod spec level and a volumeMounts entry inside the container spec.
cat < deployment-pvc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-storage
spec:
replicas: 1
selector:
matchLabels:
app: nginx-storage
template:
metadata:
labels:
app: nginx-storage
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: data-vol
mountPath: /usr/share/nginx/html
volumes:
- name: data-vol
persistentVolumeClaim:
claimName: my-pvc
EOF
kubectl apply -f deployment-pvc.yaml
kubectl get pods -l app=nginx-storage
Step 5 — Use the Local-Path StorageClass for Dynamic Provisioning
On k3s the local-path StorageClass is available by default and handles automatic PV creation when a PVC is submitted. On a kubeadm cluster, install the Rancher local-path-provisioner first.
# On kubeadm clusters — install the provisioner
kubectl apply -f
https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
# Verify the StorageClass is available
kubectl get storageclass
# Create a PVC that uses dynamic provisioning (no pre-created PV required)
cat < pvc-dynamic.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
EOF
kubectl apply -f pvc-dynamic.yaml
# The provisioner creates a PV automatically; status transitions to Bound
kubectl get pvc dynamic-pvc
kubectl get pv
Step 6 — Verify Data Persistence
Write data from inside the pod and confirm it persists after the pod is deleted and a replacement pod is scheduled.
# Write a test file inside the mounted volume
POD=$(kubectl get pod -l app=nginx-storage -o jsonpath='{.items[0].metadata.name}')
kubectl exec $POD -- sh -c "echo 'Hello persistent storage' > /usr/share/nginx/html/index.html"
# Delete the pod — the Deployment will recreate it immediately
kubectl delete pod $POD
# Wait for the new pod
kubectl get pods -l app=nginx-storage -w
# Verify the file still exists in the new pod
NEW_POD=$(kubectl get pod -l app=nginx-storage -o jsonpath='{.items[0].metadata.name}')
kubectl exec $NEW_POD -- cat /usr/share/nginx/html/index.html
Conclusion
You have provisioned both a static hostPath PersistentVolume and a dynamically provisioned volume via the local-path StorageClass on RHEL 8, bound them with PersistentVolumeClaims, and mounted the storage inside a running Deployment. The persistence test confirmed that data written to the volume survives pod deletion — the core guarantee that makes Kubernetes suitable for stateful workloads. For production deployments on RHEL 8 consider NFS-backed PVs for shared storage, the AWS EBS or GCP Persistent Disk CSI drivers for cloud environments, or Longhorn for a fully replicated block storage layer that runs entirely within the cluster.
Next steps: Deploy an application to Kubernetes on RHEL 8, Install k3s Lightweight Kubernetes on RHEL 8, and Install Helm on RHEL 8.