| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3local-path-provisioner (rancher.io/local-path) |
| Applies to | Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora Kubernetes cluster |
| Cert prep | CKA |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm. Dynamic examples use Rancher local-path-provisioner in namespace local-path-storage. |
| Privilege | Normal user for kubectl when kubeconfig is available |
| Scope | Static vs dynamic provisioning, verify a working provisioner, StorageClass fields, create classes and dynamically provisioned PVCs, Immediate vs WaitForFirstConsumer, default StorageClass, reclaim policy on dynamic PVs, volume expansion limits for this provisioner, and common failures. Does not cover CSI driver installation in depth, static PV tutorials, snapshots, cloning, storage backend administration, or provider parameter encyclopedias. |
| Related guides | Kubernetes volumes Kubernetes StatefulSets |
A StorageClass is how you publish storage policy to the cluster: which provisioner creates volumes, when they bind, and how they are reclaimed. This lab uses the Rancher local-path provisioner (rancher.io/local-path) so every class you create maps to a provisioner that is actually running.
Static vs dynamic provisioning
Both models still end with a Pod mounting a PersistentVolumeClaim. The difference is who creates the PersistentVolume.
| Static provisioning | Dynamic provisioning |
|---|---|
| An administrator creates the PV first | A provisioner creates the PV from a PVC |
| The PVC binds to an existing PV | The PVC selects a StorageClass |
| Size, path, and backend details live mainly on the PV | Policy lives on the StorageClass; parameters are provisioner-specific |
Static binding is covered in PersistentVolume and PVC. Here the PVC asks for a class, and the named provisioner builds a matching PV.
Verify a dynamic provisioner exists
A StorageClass that names a missing provisioner only produces Pending claims. Confirm classes and the provisioner Pods before you write YAML.
List StorageClass objects:
kubectl get storageclassOn this lab after local-path was installed:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 51sThe PROVISIONER column must match a controller that is actually registered. Check CSIDriver objects next — they describe CSI plugins, which is useful context even when your dynamic provisioner is an external controller rather than a CSIDriver entry:
kubectl get csidriverNAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
csi.tigera.io true true false <unset> false Ephemeral 26mCalico's csi.tigera.io is an ephemeral CSI driver for Calico, not a PersistentVolume provisioner. Do not point a PersistentVolume StorageClass at it. Confirm the local-path Deployment is Ready:
kubectl get pods -n local-path-storageNAME READY STATUS RESTARTS AGE
local-path-provisioner-6d484fd799-c79sh 1/1 Running 0 71sWhen that Pod is Running, rancher.io/local-path can create hostPath-backed PVs under /opt/local-path-provisioner on the selected node. If your cluster has no dynamic provisioner yet, install one your team supports, wait until its controller is Ready, then continue — this article does not walk CSI install depth.
Understand StorageClass fields
StorageClass is cluster-scoped. The fields you use most often are:
| Field | Role |
|---|---|
provisioner |
Name of the volume plugin or external provisioner |
parameters |
Provisioner-specific options (often empty for local-path) |
reclaimPolicy |
Copied onto dynamically created PVs (Delete or Retain) |
volumeBindingMode |
Immediate or WaitForFirstConsumer |
allowVolumeExpansion |
Whether PVC resize is allowed for this class |
parameters are not universal Kubernetes keys. Cloud CSI drivers use keys such as type or encrypted; local-path typically needs none. Always read your provisioner's docs before inventing keys.
This lab uses manifests like:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: lab-wffc
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: falseReplace rancher.io/local-path only with a provisioner string that already works in your cluster.
Create a StorageClass
Create the WaitForFirstConsumer class used for the main PVC lab:
kubectl apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: lab-wffc
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false
EOFThe API returns storageclass.storage.k8s.io/lab-wffc created when the object is accepted. List classes again:
kubectl get storageclassNAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
lab-immediate rancher.io/local-path Delete Immediate false 1s
lab-retain rancher.io/local-path Retain WaitForFirstConsumer false 0s
lab-wffc rancher.io/local-path Delete WaitForFirstConsumer false 1s
local-path rancher.io/local-path Delete WaitForFirstConsumer false 72slab-immediate and lab-retain are created the same way with different volumeBindingMode or reclaimPolicy values for the sections below. Describe one class to confirm fields:
kubectl describe storageclass lab-wffcSample output:
Name: lab-wffc
IsDefaultClass: No
Provisioner: rancher.io/local-path
Parameters: <none>
AllowVolumeExpansion: False
ReclaimPolicy: Delete
VolumeBindingMode: WaitForFirstConsumerThere is no namespace on a StorageClass. Every namespace in the cluster can reference lab-wffc by name.
Create a dynamically provisioned PVC
Work in a scratch namespace so cleanup is one delete:
kubectl create namespace sc-labnamespace/sc-lab createdCreate a claim that names the class:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wffc-pvc
namespace: sc-lab
spec:
accessModes:
- ReadWriteOnce
storageClassName: lab-wffc
resources:
requests:
storage: 256Mi
EOFThe claim is accepted immediately, but with WaitForFirstConsumer it stays Pending until a Pod mounts it. Create that consumer next:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: wffc-writer
namespace: sc-lab
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.45
command: ["sh", "-c", "echo hello-wffc > /data/out.txt; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: wffc-pvc
EOFWhen the Pod is Ready, list the claim and the generated volume:
kubectl -n sc-lab get pvc,pv -o wideNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
persistentvolumeclaim/wffc-pvc Bound pvc-c667f963-4943-473e-a2dd-5abf6d06b2ca 256Mi RWO lab-wffc 20s Filesystem
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE VOLUMEMODE
persistentvolume/pvc-c667f963-4943-473e-a2dd-5abf6d06b2ca 256Mi RWO Delete Bound sc-lab/wffc-pvc lab-wffc 2s FilesystemThe PV name is generated (pvc-<uid>). Check how it was provisioned:
kubectl get pv pvc-c667f963-4943-473e-a2dd-5abf6d06b2ca -o yamlTrimmed fields from the lab:
annotations:
pv.kubernetes.io/provisioned-by: rancher.io/local-path
local.path.provisioner/selected-node: worker01
spec:
capacity:
storage: 256Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: lab-wffc
hostPath:
path: /opt/local-path-provisioner/pvc-c667f963-4943-473e-a2dd-5abf6d06b2ca_sc-lab_wffc-pvc
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker01The reclaim policy and storage class came from the StorageClass. The provisioner pinned the volume to worker01 because that is where the Pod was scheduled.
Confirm the mount is writable:
kubectl -n sc-lab exec wffc-writer -- cat /data/out.txthello-wffcThat is the full dynamic path: StorageClass → PVC → provisioned PV → Pod mount. The next section isolates the Pending window before the Pod exists so you can recognize WaitForFirstConsumer on purpose.
Immediate vs WaitForFirstConsumer
volumeBindingMode controls when provisioning and binding start.
Immediate
With Immediate, the control plane tries to provision and bind as soon as the PVC exists, before any Pod is scheduled. That works well for many remote CSI backends where the volume is reachable from every node.
Node-local provisioners are different. Create an Immediate class and claim against local-path and the provisioner fails because no node has been selected yet:
Warning ProvisioningFailed ... failed to provision volume with StorageClass "lab-immediate": configuration error, no node was specifiedPods that use that claim stay unschedulable with pod has unbound immediate PersistentVolumeClaims. For rancher.io/local-path, prefer WaitForFirstConsumer instead of Immediate.
WaitForFirstConsumer
WaitForFirstConsumer delays provisioning until a Pod references the claim. The scheduler can then honor node selectors, affinity, and taints together with storage topology.
If you create only the PVC (before any Pod), the phase stays Pending:
kubectl -n sc-lab get pvc wffc-pvcNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
wffc-pvc Pending lab-wffc 5sDescribe the claim and read the Events:
kubectl -n sc-lab describe pvc wffc-pvcEvents:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal WaitForFirstConsumer 2s persistentvolume-controller waiting for first consumer to be created before bindingPending here is expected, not a failed provisioner. After the consumer Pod exists, the claim binds, the PV appears with nodeAffinity for the Pod's node, and the Pod becomes Ready on that same node (worker01 in this lab). Topology and scheduling stay aligned.
Configure a default StorageClass
Mark a class as default with the annotation storageclass.kubernetes.io/is-default-class: "true". Patch the class you want as the cluster default:
kubectl annotate storageclass lab-wffc storageclass.kubernetes.io/is-default-class=true --overwritekubectl get storageclassNAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
lab-immediate rancher.io/local-path Delete Immediate false 2m52s
lab-retain rancher.io/local-path Retain WaitForFirstConsumer false 2m51s
lab-wffc (default) rancher.io/local-path Delete WaitForFirstConsumer false 2m52s
local-path rancher.io/local-path Delete WaitForFirstConsumer false 4m3sCreate a PVC that omits storageClassName:
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: default-pvc
namespace: sc-lab
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 128Mi
EOFAdmission fills in the default class:
kubectl -n sc-lab get pvc default-pvcNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
default-pvc Pending lab-wffc 4sThe claim shows STORAGECLASS lab-wffc even though the YAML never set it. It stays Pending until a Pod consumes it because that class uses WaitForFirstConsumer.
Avoid multiple defaults. If several classes are marked default, Kubernetes uses the most recently created default for new PVCs without a class name.
To opt out of the default and request no class, set an empty string:
storageClassName: ""That claim does not receive the default and stays unbound unless a matching static PV exists:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
no-class-pvc Pending 2sNormal FailedBinding ... no persistent volumes available for this claim and no storage class is setUnderstand reclaim policy
reclaimPolicy on the StorageClass is copied to every dynamically provisioned PV.
| Policy | After you delete the PVC |
|---|---|
Delete |
The PV is removed and the provisioner deletes supported backend data |
Retain |
The PV moves to Released; data remains until an administrator cleans it up |
Create a Retain class and a short-lived Pod that uses it, then delete the Pod and PVC:
kubectl get pv pvc-667b7289-671a-468c-b109-de4980de687d \
-o custom-columns=NAME:.metadata.name,PHASE:.status.phase,RECLAIM:.spec.persistentVolumeReclaimPolicyAfter the PVC is gone:
NAME PHASE RECLAIM
pvc-667b7289-671a-468c-b109-de4980de687d Released RetainThe volume stays Released under Retain. With Delete (as on lab-wffc), the provisioner removes the PV after the claim is deleted. This article stops at observing the phase — reclaim and data recovery procedures belong with your storage runbooks and the static PV guide.
Enable volume expansion
Set allowVolumeExpansion: true on a StorageClass when your CSI driver supports online or offline resize:
allowVolumeExpansion: truelocal-path reports expansion as unsupported on the stock class (ALLOWVOLUMEEXPANSION false). Even if you create a class with allowVolumeExpansion: true, patching the PVC size may only queue an expand that never finishes:
Normal ExternalExpanding ... waiting for an external controller to expand this PVCCapacity on the claim stayed at the original size in this lab. Treat allowVolumeExpansion as a gate plus a driver capability — both must be present. When resize is required in production, use a CSI driver that documents expansion support.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| PVC Pending, Events show provisioner name unknown | StorageClass provisioner does not match an installed controller |
Fix the provisioner string or install the matching provisioner |
| PVC Pending, no default class | Claim omitted storageClassName and no default exists |
Create or annotate a default StorageClass, or set storageClassName explicitly |
| PVC Pending, nonexistent class | Typo in storageClassName |
Correct the name to a real StorageClass |
PVC Pending with WaitForFirstConsumer |
No Pod consumes the claim yet | Create a Pod that mounts the claim; do not treat this alone as failure |
| Immediate PVC fails with "no node was specified" | Node-local provisioner needs a selected node | Use WaitForFirstConsumer for local-path and similar backends |
| Unsupported access mode | Provisioner cannot satisfy ReadWriteMany or similar |
Match access modes to what the backend supports |
| CSI or provisioner Pods not Ready | Controller crashed or image pull failed | Repair the provisioner Deployment before creating claims |
| Invalid parameters | Provisioner-specific key rejected | Remove unknown keys; follow the provisioner's parameter list |
| Expand stuck on ExternalExpanding | Driver does not implement expansion | Use a CSI driver that supports resize, or recreate a larger volume |
For deeper Pending PVC and mount failures, continue with Fix Pending PVC and volume errors.
What's Next
- Kubernetes subPath Volume Mounts with Examples
- Fix Pending PVC, FailedMount and Volume Attachment Errors
- Kubernetes Networking Model, CNI and kube-proxy
References
- Storage Classes
- Dynamic Volume Provisioning
- Change the default StorageClass
- Persistent Volumes
- Rancher local-path-provisioner
Summary
You defined StorageClass objects against a live rancher.io/local-path provisioner, then watched a PVC select that class and receive a generated PersistentVolume. WaitForFirstConsumer kept the claim Pending until a Pod appeared, which is how node-local topology stays aligned with scheduling. Immediate mode was shown as the wrong choice for this provisioner because provisioning needs a selected node.
Default classes, empty storageClassName, and Retain versus Delete reclaim policies are cluster policy decisions that copy onto dynamic PVs. Volume expansion requires both the StorageClass flag and a capable driver — flipping the flag alone is not enough for local-path.
When claims stay Pending for the wrong reasons, check provisioner health and class names first, then follow the PVC troubleshooting guide. For static volumes without a provisioner, stay on the PersistentVolume article instead of inventing a StorageClass that nothing implements.

