| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; Kubernetes cluster with a Linux worker for the hostPath lab |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user for kubectl commands; sudo access on worker01 is required only to remove the hostPath directory |
| Scope | Static PersistentVolume and PersistentVolumeClaim binding, access modes, Pod consumption, label selectors, phases, reclaim policies, persistence verification, basic troubleshooting, and Deployment notes. Does not cover StorageClasses, dynamic provisioning, CSI driver administration, volume snapshots, PVC expansion, StatefulSet volumeClaimTemplates in depth, cloud backends, NFS server setup, local volume topology, or backup workflows. |
| Related guides | Kubernetes Pods and Pod Lifecycle Choose a Kubernetes workload resource |
This walkthrough uses the pv-lab namespace. You will:
- Create a static
hostPathPersistentVolume for lab use - Bind a PersistentVolumeClaim
- Mount the claim in a Pod
- Confirm that data survives Pod deletion
How PersistentVolumes and Claims Work
A PersistentVolume (PV) is storage made available to the cluster. A PersistentVolumeClaim (PVC) is a request for that storage. Together they work like this:
- The PV registers storage in the cluster
- The PVC requests size, access mode, and class
- The Pod consumes storage through the claim name, not by naming the PV directly
Persistent storage can outlive an individual Pod. When you delete the Pod, the claim and PV normally remain until you remove them.
The relationship in one line:
Pod → PersistentVolumeClaim → PersistentVolume → storage backend
Kubernetes volume vs PersistentVolume
Pod-level volumes are defined inside Pod YAML. PersistentVolumes are separate cluster objects bound through claims.
| Pod-level volume | PersistentVolume |
|---|---|
| Defined directly in the Pod | Separate cluster resource |
| Often tied to Pod lifetime | Exists independently of Pods |
| Suitable for temporary or configuration data | Suitable for persistent application data |
Examples: emptyDir, ConfigMap, Secret |
Consumed through a PVC |
For emptyDir, ConfigMap, Secret, projected, and hostPath mounts, see Kubernetes volumes. This article focuses on PV and PVC binding and consumption.
PersistentVolume and PVC YAML
A static PV declares capacity, access modes, reclaim policy, and a storage source. For this lab:
hostPathmaps to a directory on a single node- Kubernetes documents
hostPathPVs as suitable for single-node testing - PV node affinity constrains Pods to the node that can access the storage
This lab pins the PV to worker01 so every Pod using the claim reaches the same node directory. Run kubectl get nodes and replace worker01 if your worker uses another hostname.
Save this manifest as lab-pv.yaml:
apiVersion: v1
kind: PersistentVolume
metadata:
name: lab-pv
labels:
type: local
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker01
hostPath:
path: /tmp/pv-lab-data
type: DirectoryOrCreateKey PV fields:
spec.capacity.storage— size Kubernetes advertises for bindingspec.accessModes— supported mount modes (see the access mode table below)spec.persistentVolumeReclaimPolicy— what happens after the claim is deletedspec.storageClassName— must match the claim when you use static binding with a named classspec.nodeAffinity— limits which node can run Pods that use this volumespec.hostPath.path— node directory used in this lab (not a production pattern)
For this hostPath lab, capacity.storage: 1Gi:
- Participates in PV/PVC matching
- Does not impose a 1 GiB filesystem quota
- Does not count toward Pod ephemeral-storage consumption
A hostPath directory can still consume more node disk space. Monitor and clean the node path separately.
Save this manifest as lab-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lab-pvc
namespace: pv-lab
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
storageClassName: manualKey PVC fields:
spec.accessModes— mount mode the claim requestsspec.resources.requests.storage— minimum size the binder must satisfyspec.storageClassName— must match the PV when both objects set a class name
The claim must match an available PV:
- Requested storage and access mode must be compatible
- A claim can bind to a larger PV, but not to one smaller than its request
storageClassName: manualon both objects avoids default StorageClass dynamic provisioning during this lab
Access modes
Access modes describe storage capabilities and binding requirements. Support depends on the underlying storage system.
| Access mode | Meaning |
|---|---|
ReadWriteOnce (RWO) |
Read-write mount from a single node |
ReadOnlyMany (ROX) |
Read-only mount from multiple nodes |
ReadWriteMany (RWX) |
Read-write mount from multiple nodes |
ReadWriteOncePod (RWOP) |
Read-write mount by one Pod |
ReadWriteOncePod notes for this lab:
- Supported only for CSI volumes
- Cannot be used with the
hostPathPV in this walkthrough - Became stable in Kubernetes v1.29, but remains CSI-only
Access modes define what the volume supports and what the scheduler and attach logic can allow. They do not automatically enforce write protection after a volume is mounted in every scenario.
Create and Bind a Static PV and PVC
Follow this binding sequence:
- Create the
pv-labnamespace - Register the PV and wait until it is
Available - Create the PVC and wait until it is
Bound - Inspect both objects to confirm the binding
Create the lab namespace first:
kubectl create namespace pv-labSample output:
namespace/pv-lab createdApply the PersistentVolume:
kubectl apply -f lab-pv.yamlSample output:
persistentvolume/lab-pv createdWait for the PV:
kubectl wait --for=jsonpath='{.status.phase}'=Available pv/lab-pv --timeout=30sSample output:
persistentvolume/lab-pv condition metConfirm the PV is Available:
kubectl get pv lab-pvSample output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
lab-pv 1Gi RWO Retain Available manual <unset> 0sAvailable means the PV is registered and waiting for a matching claim.
Apply the PersistentVolumeClaim:
kubectl apply -f lab-pvc.yamlSample output:
persistentvolumeclaim/lab-pvc createdWait for binding before displaying Bound output:
kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/lab-pvc -n pv-lab --timeout=60sSample output:
persistentvolumeclaim/lab-pvc condition metList the bound claim:
kubectl get pvc -n pv-labSample output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
lab-pvc Bound lab-pv 1Gi RWO manual <unset> 2sThe claim moved from Pending to Bound once the binder found lab-pv.
Confirm the PV is now Bound to the claim:
kubectl get pv lab-pvSample output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
lab-pv 1Gi RWO Retain Bound pv-lab/lab-pvc manual <unset> 3sThe PV phase is now Bound and the CLAIM column shows pv-lab/lab-pvc.
Inspect the bound claim:
kubectl describe pvc lab-pvc -n pv-labSample output:
Name: lab-pvc
Namespace: pv-lab
StorageClass: manual
Status: Bound
Volume: lab-pv
Capacity: 1Gi
Access Modes: RWO
VolumeMode: FilesystemVolume: lab-pv confirms which PV satisfied the claim.
Inspect the PV side:
kubectl describe pv lab-pvSample output:
Name: lab-pv
Status: Bound
Claim: pv-lab/lab-pvc
Reclaim Policy: Retain
Access Modes: RWO
Capacity: 1Gi
Source:
Type: HostPath (bare host directory volume)
Path: /tmp/pv-lab-dataHow Kubernetes matches claims and volumes
The binder compares the claim against Available PVs. Main requirements:
- Requested storage size — PV capacity must be at least the claim request
- Compatible access mode — the PV must support the mode the claim requests
storageClassName— must match when both objects set a class name- Volume mode —
FilesystemversusBlockmust align - Optional label selector — claim can require PV labels
- PV availability — phase must be
Available(notBoundorReleased)
A claim can bind to a larger compatible PV, but not to one smaller than its request. This article does not cover internal control-plane binding implementation detail.
Mount the PVC and Verify Persistence
The Pod references the claim name, not the PV name:
claimNamein the Pod must match the PVC in the same namespace- The scheduler applies the bound PV's node affinity to Pods using that claim
- Pod manifests do not need their own
nodeSelectorfor this lab
Write data through the claim
Save pv-writer.yaml:
apiVersion: v1
kind: Pod
metadata:
name: pv-writer
namespace: pv-lab
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo lab-data-2026 > /data/persist.txt && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: lab-pvcspec.volumes[].persistentVolumeClaim.claimName must match the PVC name in the same namespace.
Apply the writer Pod:
kubectl apply -f pv-writer.yamlWait until the Pod is ready and the volume is mounted:
kubectl wait --for=condition=Ready pod/pv-writer -n pv-lab --timeout=90sThe Pod is ready when the volume is bound and mounted.
Verify the mounted file exists:
kubectl exec pv-writer -n pv-lab -- sh -c 'until [ -s /data/persist.txt ]; do sleep 1; done; cat /data/persist.txt'Sample output:
lab-data-2026The file was written on the mounted PVC path inside the container.
Recreate the Pod and read the data
Persistence depends on the PV and underlying storage, not on the container filesystem layer. To prove that:
- Delete the writer Pod but keep the PVC
- Create a replacement Pod that mounts the same claim
- Read the file from the mounted path with
kubectl exec
kubectl delete pod pv-writer -n pv-lab --wait=trueThe PVC and PV remain; only the Pod object is removed.
Create a reader Pod that only reads the file. Save pv-reader.yaml:
apiVersion: v1
kind: Pod
metadata:
name: pv-reader
namespace: pv-lab
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: lab-pvcApply the reader Pod and wait until it is ready:
kubectl apply -f pv-reader.yamlkubectl wait --for=condition=Ready pod/pv-reader -n pv-lab --timeout=90sRead the persisted file from the replacement Pod:
kubectl exec pv-reader -n pv-lab -- sh -c 'until [ -s /data/persist.txt ]; do sleep 1; done; cat /data/persist.txt'Sample output:
lab-data-2026The same content appears after Pod replacement because the data lives on the bound volume behind the claim.
Delete the reader Pod when finished:
kubectl delete pod pv-reader -n pv-lab --ignore-not-found=trueSelect a Static PV Using Labels
When several static PVs match size and class:
- Add labels on the PV
- Add a
selectoron the PVC to pick a specific volume
This demonstration can coexist with the main lab because:
- Selector resources use different names
gold-pvcexcludeslab-pvby requiringtier: gold
Both selector PVs use the same worker01 node affinity as lab-pv.
Create two PVs with different tier labels. Save both PV definitions as selector-pvs.yaml:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-gold
labels:
tier: gold
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker01
hostPath:
path: /tmp/pv-gold
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-silver
labels:
tier: silver
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker01
hostPath:
path: /tmp/pv-silver
type: DirectoryOrCreateRegister both selector PVs:
kubectl apply -f selector-pvs.yamlSample output:
persistentvolume/pv-gold created
persistentvolume/pv-silver createdApply the labeled claim as gold-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gold-pvc
namespace: pv-lab
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
storageClassName: manual
selector:
matchLabels:
tier: goldApply the claim:
kubectl apply -f gold-pvc.yamlWait for binding before displaying Bound output:
kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/gold-pvc -n pv-lab --timeout=60sSample output:
persistentvolumeclaim/gold-pvc condition metList the bound selector claim:
kubectl get pvc gold-pvc -n pv-labSample output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
gold-pvc Bound pv-gold 2Gi RWO manual <unset> 2sCompare both selector PVs:
kubectl get pv pv-gold pv-silverSample output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pv-gold 2Gi RWO Retain Bound pv-lab/gold-pvc manual <unset> 85s
pv-silver 2Gi RWO Retain Available manual <unset> 85spv-gold bound to the claim; pv-silver stayed Available because the selector excluded it. For general selector syntax, see Kubernetes labels and selectors.
To demonstrate Released with Retain:
- Delete the selector claim
- Wait until the bound PV reports
Released - Inspect the PV before deleting the selector PV objects
Remove the claim first:
kubectl delete pvc gold-pvc -n pv-lab --wait=trueWait until the bound PV reports Released:
kubectl wait --for=jsonpath='{.status.phase}'=Released pv/pv-gold --timeout=30sSample output:
persistentvolume/pv-gold condition metInspect the Released PV:
kubectl get pv pv-goldSample output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
pv-gold 2Gi RWO Retain Released pv-lab/gold-pvc manual 85sA PV using Retain:
- Moves to
Releasedafter the claim is deleted - Stays unavailable for another claim until it is manually reclaimed
Then delete the selector PVs:
kubectl delete pv pv-gold pv-silverPV and PVC Lifecycle
Phases describe binding and lifecycle state. They are separate from Pod STATUS.
PV and PVC phases
PersistentVolume phases
| Phase | Meaning |
|---|---|
Pending |
The PV object is being initialized and is not yet available for binding |
Available |
Not yet bound to a claim |
Bound |
Bound to a PVC |
Released |
Claim was deleted; storage has not been reclaimed |
Failed |
An automatic reclamation operation failed |
New PVs initially enter Pending, although the transition to Available is often too quick to notice with kubectl get.
PersistentVolumeClaim phases
| Phase | Meaning |
|---|---|
Pending |
No compatible PV has been bound yet |
Bound |
Claim is bound to a PV |
Lost |
Previously bound volume is no longer available |
A new claim may show Pending until a matching PV exists or the binder completes matching. After you apply a compatible PV, expect:
Pending→Boundon the claimAvailable→Boundon the volume
Reclaim policies
Reclaim policy applies to the PV and controls what happens after the bound PVC is deleted.
| Policy | Behaviour after PVC deletion |
|---|---|
Retain |
PV and underlying data are preserved for manual handling |
Delete |
PV and supported underlying storage are deleted |
Recycle is obsolete and should not be used.
With Retain, deleting the claim leaves the PV in Released. The selector demo above demonstrated this transition on pv-gold before those PV objects were deleted. To reuse a retained volume, you normally:
- Delete the PV object
- Clean the backend data
- Create a new PV before binding another claim
Backend-specific cleanup and PV reuse are outside this article.
Delete and clean up storage
Each delete step has a different effect:
- Delete the Pod — does not remove the PVC or PV; the claim stays
Boundand the data remains on the volume - Delete the PVC — with
Retain, the PV moves toReleasedand data remains until an administrator handles it; withDelete, Kubernetes may remove the PV and underlying storage when the provisioner supports deletion - Delete the PV — removes the Kubernetes record; effect on underlying files depends on volume type and whether a claim still references the volume
Troubleshoot a PVC Stuck in Pending
Start troubleshooting with:
kubectl describe pvcfor claim Eventskubectl get pvfor available volumes
kubectl describe pvc <claim-name> -n <namespace>List available PVs:
kubectl get pv| Symptom | Likely cause | Fix |
|---|---|---|
PVC Pending, no matching PV |
No Available PV with enough capacity |
Create or free a compatible PV |
PVC Pending, requested size exceeds PV capacity |
No compatible PV is large enough | Reduce the claim request or create a larger PV |
PVC Pending, access mode mismatch |
PV does not support requested mode | Align accessModes on PV and PVC |
PVC Pending, class mismatch |
storageClassName differs between PV and PVC |
Set the same class on both or use matching empty string for static binding |
PVC Pending, selector mismatch |
No PV carries required labels | Add labels to the intended PV or relax the selector |
PVC Pending, volume mode mismatch |
Filesystem versus Block differ |
Match volumeMode on both objects |
Pod Pending after PVC is Bound |
No schedulable node satisfies the PV node affinity, or the pinned node is unavailable or unschedulable | Check Pod Events; confirm worker01 is Ready, schedulable, and its kubernetes.io/hostname label matches the PV |
| Pod mount error, PVC not found | Pod and PVC in different namespaces | Create the claim in the Pod namespace or reference the correct name |
PV Released after PVC deletion |
Expected with Retain |
Manually reclaim the storage; normally delete the PV object, clean the backend data, and create a new PV before reuse |
Use PVCs with Deployments
A Deployment Pod can mount a PVC using the same pattern as a standalone Pod:
persistentVolumeClaimunderspec.volumes- Matching
volumeMountson the container
Sharing one claim across replicas works only when:
- The storage backend supports the attachment pattern
- The access mode allows the intended number of node attachments
- You do not assume one
ReadWriteOnceclaim safely backs many replicas spread across nodes
For per-replica storage identity and ordered rollout with claims, see Kubernetes StatefulSets.
What's Next
- Kubernetes StorageClass and Dynamic Volume Provisioning
- Kubernetes subPath Volume Mounts with Examples
- Fix Pending PVC, FailedMount and Volume Attachment Errors
References
Summary
You registered a static PersistentVolume, confirmed it was Available, created a matching PersistentVolumeClaim, and then confirmed that both objects became Bound. The Pod mounted the claim by name through persistentVolumeClaim.claimName, and data written to the mount path survived Pod deletion because it lives on the volume behind the claim, not in the container image layer.
Access modes and reclaim policy matter as much as YAML shape:
ReadWriteOncepermits read-write mounting from a single nodeRetainleaves aReleasedPV after claim deletion until an administrator handles reuse- Label selectors let you pick a specific static PV when several compatible volumes exist
For Pod-level scratch and configuration mounts, use Pod volumes instead of PV objects. When replicas need stable storage identity or ordered binding, move to StatefulSet patterns. StorageClasses and dynamic provisioning build on the same PVC model for production clusters.

