How to Configure Kubernetes Persistent Volumes and Storage Classes on RHEL 7
By default, storage inside a Kubernetes Pod is ephemeral — when a Pod restarts or is rescheduled, all data written to the container filesystem is lost. For applications like databases, file servers, and message queues that require durable storage, Kubernetes provides a storage abstraction layer built around three objects: PersistentVolume (PV), which represents a piece of storage provisioned in the cluster; PersistentVolumeClaim (PVC), which is a request for storage made by a workload; and StorageClass, which enables dynamic provisioning so that PVs are created on demand. This tutorial covers all three objects in detail, demonstrates hostPath and NFS volume types suitable for RHEL 7, explains access modes, and shows how to configure dynamic provisioning using the local-path-provisioner for k3s clusters.
Prerequisites
- A running Kubernetes cluster (kubeadm or k3s) on RHEL 7
kubectlconfigured with access to the cluster- For NFS volumes: an NFS server reachable from all cluster nodes
- For local-path dynamic provisioning: a k3s cluster (the provisioner is included by default)
- Root access to create directories on the host for hostPath volumes
Step 1: Understand PersistentVolume Access Modes
Every PersistentVolume is created with one or more access modes that define how the volume can be mounted across nodes. The three standard modes are:
- ReadWriteOnce (RWO) — The volume can be mounted read-write by a single node. Most block storage types (local disk, cloud block volumes) support this.
- ReadOnlyMany (ROX) — The volume can be mounted read-only by many nodes simultaneously.
- ReadWriteMany (RWX) — The volume can be mounted read-write by many nodes simultaneously. Requires a network filesystem such as NFS or CephFS.
# Access modes are specified in both PV and PVC:
# RWO = ReadWriteOnce
# ROX = ReadOnlyMany
# RWX = ReadWriteMany
Step 2: Create a hostPath PersistentVolume
A hostPath volume maps a directory on the node’s filesystem into the Pod. This is the simplest volume type and is suitable for single-node clusters or development environments. It should not be used in production multi-node clusters because the data is tied to a specific node.
# Create the host directory
mkdir -p /data/k8s-storage/my-pv-data
chmod 777 /data/k8s-storage/my-pv-data
cat > pv-hostpath.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-hostpath-100m
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: "/data/k8s-storage/my-pv-data"
type: DirectoryOrCreate
EOF
kubectl apply -f pv-hostpath.yaml
kubectl get pv pv-hostpath-100m
The persistentVolumeReclaimPolicy controls what happens when a PVC is deleted. Retain keeps the data for manual cleanup; Delete removes the underlying storage resource; Recycle scrubs the volume (deprecated).
Step 3: Create an NFS PersistentVolume
NFS volumes allow data to be shared across multiple nodes and support ReadWriteMany access. Install the NFS client utilities on all nodes first.
# Install NFS utilities on all cluster nodes
yum install -y nfs-utils
# Enable and start the rpcbind service
systemctl enable rpcbind
systemctl start rpcbind
cat > pv-nfs.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-shared
spec:
storageClassName: nfs-storage
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /exports/k8s-shared
server: 192.168.1.50
readOnly: false
EOF
kubectl apply -f pv-nfs.yaml
kubectl get pv
Step 4: Create a PersistentVolumeClaim
A PVC is how a workload requests storage. Kubernetes matches the PVC to an available PV based on the requested capacity, access mode, and StorageClass. Once bound, the PVC and PV are linked exclusively (for RWO claims).
cat > pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
namespace: default
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
EOF
kubectl apply -f pvc.yaml
# Check the PVC status — it should show "Bound"
kubectl get pvc my-pvc
Expected output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Bound pv-hostpath-100m 1Gi RWO manual 10s
Step 5: Mount a PVC in a Pod Spec
To use persistent storage in a Pod, reference the PVC name in the Pod’s volumes section and add a volumeMount in the container spec.
cat > pod-with-pvc.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: nginx-with-storage
namespace: default
spec:
volumes:
- name: web-content
persistentVolumeClaim:
claimName: my-pvc
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
volumeMounts:
- name: web-content
mountPath: /usr/share/nginx/html
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
EOF
kubectl apply -f pod-with-pvc.yaml
kubectl get pod nginx-with-storage
# Verify the mount inside the pod
kubectl exec -it nginx-with-storage -- df -h /usr/share/nginx/html
Step 6: Configure a StorageClass for Dynamic Provisioning
A StorageClass defines a “profile” of storage with a specific provisioner. When a PVC references a StorageClass and no matching PV exists, the provisioner automatically creates one — this is called dynamic provisioning and eliminates the need to pre-create PVs manually.
cat > storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner # static provisioning placeholder
volumeBindingMode: WaitForFirstConsumer # bind only when Pod is scheduled
reclaimPolicy: Delete
allowVolumeExpansion: true
parameters:
type: ssd
EOF
kubectl apply -f storageclass.yaml
kubectl get storageclass
Step 7: Enable Dynamic Provisioning with local-path-provisioner (k3s)
k3s ships with local-path-provisioner already deployed, providing dynamic provisioning backed by a local directory on the node. It creates PVs automatically when PVCs are submitted.
# Verify local-path-provisioner is running in k3s
kubectl get pods -n kube-system | grep local-path
# Check the default StorageClass created by k3s
kubectl get storageclass
# Expected output
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false
Now create a PVC that references the local-path StorageClass and it will be provisioned automatically:
cat > pvc-dynamic.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
namespace: default
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
EOF
kubectl apply -f pvc-dynamic.yaml
# PVC will remain "Pending" until a Pod claims it (WaitForFirstConsumer)
kubectl get pvc dynamic-pvc
Step 8: Inspect PV and PVC Status with kubectl get
# List all PersistentVolumes
kubectl get pv
# List all PersistentVolumeClaims in all namespaces
kubectl get pvc --all-namespaces
# Show detailed information about a PV
kubectl describe pv pv-hostpath-100m
# Show detailed information about a PVC
kubectl describe pvc my-pvc
# Check events for storage-related issues
kubectl get events --sort-by=.metadata.creationTimestamp | grep -i pvc
Key PVC status values:
- Pending — No matching PV found yet (or waiting for WaitForFirstConsumer)
- Bound — Successfully matched and attached to a PV
- Lost — The bound PV has been deleted or is unavailable
Step 9: Use PVC in a StatefulSet for Databases
StatefulSets are the preferred workload type for stateful applications like MySQL. They support volumeClaimTemplates, which automatically create a dedicated PVC for each Pod replica.
cat > statefulset-mysql.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
serviceName: mysql
replicas: 1
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "ChangeMe!"
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
EOF
kubectl apply -f statefulset-mysql.yaml
kubectl get statefulset mysql
kubectl get pvc
Step 10: Clean Up Persistent Storage
# Delete the pod using the PVC first
kubectl delete pod nginx-with-storage
# Delete the PVC (PV reclaim policy determines what happens to the PV)
kubectl delete pvc my-pvc
# If reclaim policy is Retain, the PV will stay in "Released" state
# Manually delete the PV to free it
kubectl delete pv pv-hostpath-100m
# Clean up the host directory
rm -rf /data/k8s-storage/my-pv-data
Persistent storage is a fundamental requirement for any real-world Kubernetes deployment, and RHEL 7 clusters are no exception. You have explored all three core storage objects — PersistentVolume, PersistentVolumeClaim, and StorageClass — and implemented static provisioning with both hostPath and NFS volumes as well as dynamic provisioning using the local-path-provisioner built into k3s. Understanding access modes (RWO, ROX, RWX) helps you select the right volume type for each workload. As your infrastructure grows, consider integrating a more robust dynamic provisioner such as the NFS Subdir External Provisioner, Rook-Ceph for a distributed storage cluster, or a cloud-native CSI driver to provide block and object storage directly within Kubernetes.