A Kubernetes Persistent Volume is an abstraction over real storage that outlives a Pod's own lifecycle. A Pod never gets this volume directly, it claims it through a PersistentVolumeClaim, or PVC. Together, PV and PVC form the storage foundation that databases, message brokers and anything else that needs to remember data beyond a restart is built on.
I run production-grade Kubernetes clusters with k3s on Hetzner Cloud and cover storage in detail in chapter 6 of my Kubernetes book published by Rheinwerk. Most storage problems I see in projects do not come from the PV itself, they come from the wrong choice of access mode or reclaim policy. You can find all my Kubernetes articles collected on the Kubernetes page.
Kubernetes Persistent Volume and Persistent Volume Claim: how they work together
Think of a Persistent Volume as an external hard drive sitting somewhere in the data center. Whether it is backed by network storage, cloud block storage or a local disk does not matter to your application, because access through the PV always looks the same. The PVC is a Pod's request list for exactly that volume: how much storage, which access mode, from which storage class.
An exclusive connection forms between PV and PVC as soon as the two match. Here is an example: a Grafana Pod needs to store its dashboards and settings permanently. To do that, you first create a PV that provides 5 gigabytes on network storage.
apiVersion: v1
kind: PersistentVolume
metadata:
name: grafana-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
nfs:
path: /export/grafana
server: 10.0.1.20
The matching PVC requests those properties. Access mode and storage class have to match, and the requested size must not exceed the PV's capacity. If one of them does not fit, the PVC stays stuck in the Pending state, waiting for a volume that never arrives.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-pvc
spec:
storageClassName: ""
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
A PV moves through visible states along the way. Right after creation, it sits at Available. Once a matching PVC claims it, it switches to Bound. Delete the PVC again and the PV falls back to Released, but keeps its data until you step in manually. That is deliberate: Kubernetes never silently deletes data just because a PVC disappeared.
How Persistent Volumes actually get used inside a StatefulSet, for example through volumeClaimTemplates, is covered in Kubernetes StatefulSet vs Deployment.
Access modes: who can access a volume, and how
Not every storage type supports every access mode, since that depends on the underlying storage. Still, it is worth knowing all four modes before you write a PVC, because getting one wrong later leads to Pods that simply never start.
| Access mode |
Meaning |
| ReadWriteOnce |
Read and write from exactly one node. Multiple Pods on that same node can still access it at once. |
| ReadOnlyMany |
Multiple nodes can read at the same time, none of them can write. |
| ReadWriteMany |
Multiple nodes can read and write at the same time, for example a shared upload folder. |
| ReadWriteOncePod |
Guarantees that only a single Pod in the entire cluster accesses the volume, not just one per node. |
In practice, ReadWriteOnce is what you hit most often, because most block storage solutions only support that mode. You need ReadWriteMany mainly when several Pods have to read and write the same files, such as uploaded images on a website. One thing matters here: an access mode is a request, not a promise. Writing ReadWriteMany into a PVC does not mean your CSI driver can actually do write access from several nodes. If it cannot, the claim stays unbound or the second Pod hangs on mount. File systems such as NFS or CephFS can do it, classic block storage usually cannot. Which storage type actually offers which access mode is fully listed in the Kubernetes documentation on access modes.
Static or dynamic: storage class and reclaim policy
The two examples above show a static PV: you create it yourself before any PVC claims it. In practice this works differently, because almost nobody wants to hand-write a PV for every application. Through a storage class, Kubernetes can create the volume itself to match a PVC.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: hetzner-volumes
provisioner: csi.hetzner.cloud
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
csi.storage.k8s.io/fstype: ext4
Two settings deserve special attention here. allowVolumeExpansion lets you grow the storage of an existing PVC later without having to rebuild the application. volumeBindingMode decides exactly when the volume gets created.
| volumeBindingMode |
Behavior |
| Immediate |
The PV is created right away when the PVC is made, regardless of which Pod ends up using it. |
| WaitForFirstConsumer |
Kubernetes waits until a Pod actually needs the PVC, then creates the PV in the correct zone. |
WaitForFirstConsumer is almost always the right choice for node-bound storage, because otherwise a volume can end up in a zone where no matching node is later free for the Pod.
Just as important is the reclaim policy, which defines what happens to a PV once its PVC disappears. Retain keeps the PV and its data around and requires manual cleanup. Delete is the default for dynamically created PVs and deletes the volume together with the PVC, permanently. A third option, Recycle, is considered legacy and should be replaced by dynamic PVs. If you run production data on a storage class with a Delete policy, keep in mind that an accidentally deleted PVC takes the data with it, irreversibly.
Storage on Hetzner: CSI drivers, Longhorn and local-path
For my own clusters on Hetzner Cloud, I generally have three ways to provide storage, and I pick a different one depending on the application. The official Hetzner CSI driver attaches Hetzner Cloud Volumes as network storage, exactly as in the storage class example above. The volume is bound to a zone but follows the Pod across nodes within that zone, which is the simplest solution for most databases and applications.
Longhorn takes a different approach: it turns each node's local storage into a distributed, replicated block storage system and handles replication across nodes itself. That becomes interesting when you do not want to rely on cloud storage, or when you need redundancy beyond what the cloud provider already offers, but it costs you extra resources and operational effort for the Longhorn system itself.
For anything that is not production-critical, such as caches or test environments, the local-path provisioner is often enough. It creates PVs directly on a node's local file system. It is simple and fast, but it ties the Pod firmly to that one node, since the volume cannot move with it. Whether self-managed storage or a switch to a managed cloud solution makes more sense for a given application is something I weigh up in Kubernetes On-Premises or Cloud?.
Temporary volumes: emptyDir, ephemeral and projected volumes
Not every application needs data that outlives a Pod. For a cache or scratch space, an ephemeral volume is enough, one that is created with the Pod and disappears with it too. The simplest case is emptyDir, an empty folder the kubelet creates at startup.
volumes:
- name: cache
emptyDir:
sizeLimit: 500Mi
medium: Memory
With medium: Memory, you place the volume directly in RAM, which is much faster than disk but counts against the Pod's resource limit. For more demanding cases there are CSI-based ephemeral volumes and generic ephemeral volumes, which, depending on the driver, can even support snapshots or pre-populated data.
A related but separate feature is projected volumes. They combine several sources such as ConfigMaps, Secrets or the downward API under a single mount path, instead of needing a separate mount for each source. Your application then finds all its configuration files in one place, which cleans up the YAML manifests noticeably when there are many small configuration values.
Backing up volumes: snapshots as one building block
A volume snapshot is a point-in-time copy of your PV, comparable to a cloud block storage snapshot you may already know from other providers. Kubernetes models this through three related objects: a VolumeSnapshot is your request, similar to a PVC, a VolumeSnapshotContent is the actual data copy, and a VolumeSnapshotClass defines which driver performs the snapshot.
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: grafana-pvc-snapshot
spec:
volumeSnapshotClassName: hetzner-snapshot
source:
persistentVolumeClaimName: grafana-pvc
Snapshots are most useful as a quick checkpoint before a risky change, such as a database update, or to pull a copy for troubleshooting without touching the live system. They do not replace a full backup strategy, though: a snapshot often lives on the same storage system as the original and does not protect you if that system fails. How you back up volumes and overall cluster state, including etcd, robustly is covered in Kubernetes Backup: etcd and Volumes.
Frequently asked questions
What is the difference between a Persistent Volume and a storage class?
A Persistent Volume is a concrete piece of storage that a Pod claims through a PVC. A storage class, on the other hand, describes how Kubernetes should create such a volume on demand, including the driver, reclaim policy and binding behavior. Without a storage class, you would have to create every PV by hand.
What access modes exist for a Kubernetes Persistent Volume?
Four: ReadWriteOnce for read and write access from one node, ReadOnlyMany for read access from multiple nodes, ReadWriteMany for read and write access from multiple nodes, and ReadWriteOncePod, which limits access to a single Pod cluster-wide. Which mode is available depends on the storage type.
What does WaitForFirstConsumer mean for a storage class?
It delays creating the Persistent Volume until a Pod actually needs the PVC. That way, Kubernetes places the volume in the right zone or on the right node from the start, instead of prematurely picking a spot where no matching Pod can later land.
What happens to a PV when I delete its PVC?
That depends on the reclaim policy. With Retain, the PV and its data stay in place and switch to the Released state until you step in manually. With Delete, the default for dynamically created PVs, the PV and its data are removed immediately and permanently.
Do I need volume snapshots if I already run backups?
Not necessarily. Snapshots are mainly useful for quick checkpoints, for example before a risky update. For a complete backup strategy that can restore data elsewhere, you still need a solution that stores data outside the original storage system.
Where to go next
If your application needs several instances with their own identity and their own volume, it is worth looking at the StatefulSet, which creates Persistent Volumes per Pod automatically through volumeClaimTemplates: Kubernetes StatefulSet vs Deployment. If you want to figure out whether your storage should be self-managed at all or a managed solution makes more sense, Kubernetes On-Premises or Cloud? will help.
My suggestion for today: create a PVC with a storage class of your choice, check its state with kubectl get pv,pvc, then delete the PVC as a test to see how the configured reclaim policy actually behaves.
In full detail with all examples, from storage types through CSI drivers to snapshots and projected volumes, this is covered in chapter 6 of my book "Kubernetes: Practical Guide for Developers and DevOps Teams" (Rheinwerk Computing). You can find all the information about the book on my Kubernetes page.