Storage: Volumes, PV, PVC and StorageClass

K
Kai··5 min read

So far our application is stateless — a pod dies, a new pod is built, nothing is lost. But what about a database, files users upload, or data that needs to be kept? Anything written inside the pod will evaporate with the pod when it dies. This article solves that: how to store data that outlives the pod's lifecycle.

The problem: the pod's filesystem is ephemeral

By default, the container filesystem is tied to the pod's lifecycle. A pod gets deleted or rebuilt (remember self-healing, rolling update in Article 4) → everything written inside disappears. For a stateless app that's fine; for data that must be kept it's a disaster. Kubernetes has volumes to attach a storage area to a pod — and crucially, the durable kind of volume.

The simplest volume is emptyDir: an empty directory that lives with the pod, used to share files between containers within the same pod or as a temporary cache. But emptyDir is also lost when the pod dies — not what we need for durable data. What we need is a PersistentVolume.

Three concepts: PV, PVC, StorageClass

Kubernetes separates the demand for storage from the supply of storage — like booking a hotel room without needing to know which specific room:

   PVC  (PersistentVolumeClaim)   "I need 100Mi, read-write"   ← you (dev) declare
        │  Kubernetes matches
        ▼
   PV   (PersistentVolume)        real 100Mi disk              ← infrastructure provides
        │  provisioned by
        ▼
   StorageClass                   "how to provision disk" (disk type)  ← admin defines
  • PersistentVolume (PV) — a real piece of storage in the cluster (an EBS disk, a directory on the host, an NFS share...). It's a cluster-scoped resource.
  • PersistentVolumeClaim (PVC) — your storage request: "need 100Mi, ReadWriteOnce access mode". You work with the PVC, not the PV directly.
  • StorageClass — defines how to provision PVs dynamically. When you create a PVC, the StorageClass automatically creates a matching PV — called dynamic provisioning. No need to create PVs by hand.

This separation matters: the dev only says "I need this much capacity"; how that disk comes to exist (EBS on AWS, hostpath on minikube...) is the StorageClass's job. The same PVC runs on any infrastructure.

minikube ships with a default StorageClass

kubectl get storageclass
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   AGE
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           14m

minikube provides standard (marked default), using minikube-hostpath — stored on the node's disk. Because there's a default StorageClass, we only need to create a PVC and a PV appears automatically.

Create a PVC and watch it get a PV

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 100Mi
kubectl apply -f pvc.yaml
kubectl get pvc,pv
NAME                             STATUS   VOLUME             CAPACITY   ACCESS MODES   STORAGECLASS
persistentvolumeclaim/data-pvc   Bound    pvc-04182fed...    100Mi      RWO            standard

NAME                             CAPACITY   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS
persistentvolume/pvc-04182fed... 100Mi      Delete           Bound    default/data-pvc   standard

The magic of dynamic provisioning: we only created a PVC, but a PV (pvc-04182fed...) appears on its own and both are in the Bound state (paired up). The StorageClass quietly provisioned the disk. ACCESS MODES: RWO is ReadWriteOnce — one node attaches read-write at a time (enough for most cases; for many nodes writing at once there's ReadWriteMany, which needs a backend that supports it).

Experiment: data outlives the pod

This is the proof. Attach the PVC to a pod, write data, delete the pod, build a new pod reusing that PVC, then read it back.

# excerpt of the pod spec
    volumeMounts:
      - name: data
        mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data-pvc
kubectl apply -f pod-writer.yaml
# write one line into the volume
kubectl exec writer -- sh -c 'echo "data outlives the pod - $(date)" > /data/note.txt; cat /data/note.txt'
data outlives the pod - Sat May 23 11:16:26 UTC 2026

Now delete the pod then build a new pod (same PVC):

kubectl delete pod writer
kubectl apply -f pod-writer.yaml
kubectl exec writer -- cat /data/note.txt
data outlives the pod - Sat May 23 11:16:26 UTC 2026

The exact same line, intact. The pod died and was reborn completely new, but the data in the PVC stayed put. This is exactly what emptyDir or the pod filesystem cannot do. Databases, uploads, logs that need to be kept — all rely on this mechanism.

reclaim policy: what happens when you delete the PVC

The RECLAIM POLICY: Delete column above means: when you delete the PVC, the PV (and its data) is deleted too. Suitable for dev/test. Production usually sets Retain so the PV (and data) is kept when the PVC is gone, avoiding accidentally deleting precious data. This is a knob you need to be aware of before deleting a PVC in a real environment.

Wrap-up

The pod filesystem is ephemeral — lost when the pod dies, so data that needs keeping must live outside the pod. Kubernetes separates the PVC (the demand: "need this much capacity") from the PV (the real disk), with StorageClass provisioning PVs dynamically (dynamic provisioning) — the dev only works with the PVC, and it runs on any infrastructure. Create a PVC and you automatically get a Bound PV; attach it to a pod via persistentVolumeClaim. The delete-then-recreate experiment proves data outlives the pod. Mind the reclaim policy (Delete vs Retain) so you don't delete data by accident. (StatefulSet in Article 12 builds on this exact mechanism for stateful apps.)

The application now has configuration and storage. There's one piece left to serve real users: getting HTTP from the outside in, routing by domain/path. Article 9: Ingress.