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)
  • kubectl configured and pointing at the cluster
  • For the StorageClass section: k3s with the bundled local-path provisioner, 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.