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
  • kubectl configured 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.