2026年11月13日
2026年11月13日
Kubernetes PersistentVolume and PVC: Dynamic and Static Provisioning
This article is a localized edition of the Japanese original. Read the Japanese original
Files inside a Kubernetes Pod disappear when the Pod is deleted or recreated. Data you need to keep, such as database data or uploaded files, goes on a PersistentVolume (PV).
This article sorts out how PVs relate to PersistentVolumeClaims (PVCs) and explains two ways to attach storage to a Pod.
- Dynamic provisioning: when you create a PVC, a PV is created automatically according to a StorageClass. This is the usual approach on managed Kubernetes in the cloud and on most clusters
- Static provisioning: an administrator creates PVs by hand and PVCs bind to them
The manifests and output were checked as of October 2026 on Kubernetes 1.34 (k3s, whose default StorageClass is local-path).
How PV, PVC and StorageClass fit together
| Resource | Role | Created by |
|---|---|---|
| PersistentVolume (PV) | Represents actual storage (a disk, NFS, cloud block storage and so on). A cluster-wide resource | An administrator, or automatically via a StorageClass |
| PersistentVolumeClaim (PVC) | A request for "storage of this size with this access mode". A namespaced resource | Whoever runs the app |
| StorageClass | The kind of PV to create automatically (provisioner and parameters) | An administrator |
A Pod references a PVC by name, and the PV bound to that PVC is mounted. If you recreate the Pod and use the same PVC, you see the same data.
Check the StorageClass first
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 2s
If there is a StorageClass marked (default), creating a PVC is enough to get a PV created automatically (dynamic provisioning). The names and provisioners shown differ by cluster. If nothing is shown, prepare PVs yourself with static provisioning, or install a storage driver (CSI driver).
Use dynamic provisioning
Write a PVC and a Pod that mounts it in one file (web.yaml).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: nginx
image: nginx:1.28
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: web-data
Because storageClassName is not specified, the default StorageClass is used. To use a specific StorageClass, add storageClassName: <name> to the PVC's spec.
kubectl apply -f web.yaml
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
web-data Bound pvc-843d1db2-d309-40e5-b160-93506f1dc039 1Gi RWO local-path <unset> 18s
When STATUS becomes Bound, a PV has been created and bound to the PVC. If the StorageClass's VOLUMEBINDINGMODE is WaitForFirstConsumer, the PVC stays Pending until a Pod that uses it is created. That is normal.
Confirm that the data persists
Create a file inside the Pod, then delete and recreate the Pod and check that the file is still there.
kubectl exec web -- sh -c 'echo hello-pv > /usr/share/nginx/html/index.html'
kubectl delete pod web
kubectl apply -f web.yaml
kubectl exec web -- cat /usr/share/nginx/html/index.html
If the last command prints hello-pv, the data is stored on the PV.
In real deployments, it is more common to use PVCs from a Deployment (or from a StatefulSet's volumeClaimTemplates when each Pod needs its own volume) than to create Pods directly. The way you reference the PVC (persistentVolumeClaim under volumes) is the same.
Use static provisioning
This is how you create PVs yourself, for example to use an existing NFS server or disk. The following example is a hostPath PV that uses a directory on the node. hostPath is for testing on a single node: if the Pod runs on another node the data is not visible, so do not use it in production.
apiVersion: v1
kind: PersistentVolume
metadata:
name: manual-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /srv/k8s-data/manual-pv
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: manual-pvc
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
kubectl apply -f manual.yaml
kubectl get pv manual-pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
manual-pv 5Gi RWO Retain Bound default/manual-pvc manual <unset> 3s
The key points are:
- A PV needs exactly one volume type, such as
hostPath,nfs,csiorlocal. A PV without one cannot be created and fails withspec: Required value: must specify a volume type - A PVC binds to a PV with the same
storageClassNamethat satisfies its size and access mode requirements - If you omit
storageClassNamein the PVC, the default StorageClass is used, a new PV is created automatically, and the PVC does not bind to your hand-made PV. When using static PVs, write the samestorageClassNamein both the PV and the PVC (matching a name such asmanualis enough, even without creating a StorageClass resource)
Using local volumes
To use a disk attached to a node in production, use a local volume instead of hostPath. A local PV must specify the node that has the disk with nodeAffinity, and the StorageClass should use WaitForFirstConsumer.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-1
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/disks/vol1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-1
Replace worker-1 with a node name shown by kubectl get nodes, and /mnt/disks/vol1 with a directory actually mounted on that node. kubernetes.io/no-provisioner does not create PVs automatically, so prepare one PV per disk.
Access modes
| Access mode | Short form | Meaning |
|---|---|---|
ReadWriteOnce |
RWO | Read-write from one node (multiple Pods on the same node can use it) |
ReadOnlyMany |
ROX | Read-only from many nodes |
ReadWriteMany |
RWX | Read-write from many nodes |
ReadWriteOncePod |
RWOP | Read-write by a single Pod only |
Which access modes are available depends on the storage type. Cloud block storage, for example, usually supports only RWO; if Pods on multiple nodes need to read and write the same data (RWX), use a shared file system such as NFS.
Reclaim policy (what happens to the PV when the PVC is deleted)
The PV's persistentVolumeReclaimPolicy decides what happens to the PV and its data when the PVC is deleted.
| Policy | When the PVC is deleted |
|---|---|
Retain |
The PV becomes Released and the data stays. An administrator cleans up by hand |
Delete |
The PV and the underlying storage (such as a cloud disk) are deleted too |
PVs created by dynamic provisioning inherit the StorageClass's reclaimPolicy (usually Delete). It is common for the setup to be such that deleting the PVC also deletes the data, so check before deleting a PVC that holds important data. Recycle is deprecated; do not use it.
A Retain PV becomes Released when its PVC is deleted, and as-is it does not bind to a new PVC. If you have checked the data and want to reuse the same PV, remove the reference to the old PVC (claimRef) and it returns to Available.
kubectl patch pv manual-pv --type json -p '[{"op":"remove","path":"/spec/claimRef"}]'
Increase capacity
If the StorageClass has allowVolumeExpansion set to true, you can grow a volume just by raising spec.resources.requests.storage on the PVC.
kubectl patch pvc web-data -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
You cannot shrink a volume. On a StorageClass where allowVolumeExpansion is false (or unset), this change is rejected.
When a PVC stays Pending
The reason is shown in Events at the end of kubectl describe pvc.
kubectl describe pvc PVC_NAME
Example in Events |
Cause and fix |
|---|---|
waiting for first consumer to be created before binding |
The StorageClass uses WaitForFirstConsumer and no Pod uses the PVC yet. Creating the Pod moves it forward |
storageclass.storage.k8s.io "manual" not found |
There is neither a PV nor a StorageClass matching the given storageClassName. Check that the PV's size, access mode and storageClassName match the PVC |
no persistent volumes available for this claim and no storage class is set |
storageClassName is empty, there is no default StorageClass, and no PV meets the requirements |
Waiting for a volume to be created either by the external provisioner ... keeps appearing |
The provisioner (CSI driver or similar) is not running or is failing. Check the logs of the provisioner's Pod |
Check the list and state of PVs with kubectl get pv. Available means unused, Bound in use, Released the PVC has been deleted, and Failed automatic reclamation failed.
Summary
- A Pod references a PVC, and the PV bound to that PVC is mounted. The data survives Pod recreation
- With a default StorageClass, creating a PVC is enough to have a PV created automatically (dynamic provisioning)
- When creating PVs by hand, specify the volume type (
hostPath,nfs,localand so on) and use the samestorageClassNameon the PV and the PVC - Access modes depend on the storage type. Read-write from multiple nodes (RWX) requires a shared file system
- Dynamically created PVs are often
Delete, so deleting the PVC deletes the data. Protect important data withRetainand backups - If a PVC stays
Pending, check the reason inEventsofkubectl describe pvc
Related articles:
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)