This is the fourth post in our Kubernetes fundamentals series, building on Pods and Deployments and ConfigMaps and Secrets.
We covered this exact problem for plain Docker containers earlier in this blog: a container's own writable filesystem layer disappears the moment the container is removed. Kubernetes inherits that same disposability at the Pod level — a replaced Pod gets a brand new, empty filesystem — and adds its own set of objects for solving it properly at cluster scale.
The simplest case: emptyDir
apiVersion: v1
kind: Pod
metadata:
name: cache-pod
spec:
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: cache-volume
mountPath: /tmp/cache
volumes:
- name: cache-volume
emptyDir: {}An emptyDir volume is created empty when the Pod starts and shares that storage across every container in the Pod — genuinely useful for temporary scratch space or for one container in a Pod to pass data to another. It is not a durable storage solution: it's deleted the moment the Pod itself is removed, exactly like the underlying container filesystem it's meant to improve on. If a Pod restarts (rather than being removed and recreated), the emptyDir typically does survive — but that distinction is fragile enough that it shouldn't be relied on for anything that actually matters.
PersistentVolumes and PersistentVolumeClaims: storage that outlives a Pod
For real durability, Kubernetes separates the request for storage from the actual provisioning of it — two objects, working together:
# PersistentVolumeClaim (PVC) — the request
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GiapiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: db-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: db-storage
persistentVolumeClaim:
claimName: db-dataA PersistentVolumeClaim (PVC) is a request: "I need 10Gi of storage, readable and writable by one Pod at a time." A PersistentVolume (PV) is the actual underlying storage that gets bound to satisfy that request — an AWS EBS volume, a GCP persistent disk, an NFS share, depending on the cluster's environment. Crucially, the PVC — and the real data behind it — survives the Pod being deleted and recreated. A new Pod referencing the same PVC reattaches to the exact same underlying storage, data intact.
Why database Pods use StatefulSets, not Deployments
Notice the example above uses a StatefulSet, not a Deployment — this is the detail most people miss the first time they try to run a database in Kubernetes. A Deployment's replicas are meant to be interchangeable — any replica can be replaced by any other, with no individual identity. A database replica very much has an identity: it has specific data attached to it, and swapping one for an identically-configured but empty replacement is not the same operation at all.
A StatefulSet gives each replica a stable, unique identity (db-0, db-1, not an arbitrary generated suffix) and — this is the important part — its own dedicated PVC, created automatically per replica and reattached to that same specific replica if it's ever recreated. This is why stateful workloads (databases, message queues, anything where individual replica identity and attached storage genuinely matter) use StatefulSets, while stateless application servers use Deployments.
StorageClasses: how the actual provisioning happens
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "4000"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumerapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GiA StorageClass tells Kubernetes how to dynamically provision storage when a PVC requests it — which cloud disk type, which performance tier — rather than requiring a cluster administrator to manually pre-create PersistentVolumes for every possible request in advance. Referencing storageClassName: fast-ssd in a PVC triggers automatic provisioning of exactly the kind of underlying disk that StorageClass describes, the moment the PVC is created.
reclaimPolicy deserves specific attention: Delete (the common default) means the underlying cloud disk is destroyed when its PVC is deleted — appropriate for genuinely disposable data, actively dangerous for anything that isn't. Retain keeps the underlying storage even after the PVC referencing it is deleted, at the cost of needing manual cleanup later. Getting this backwards in either direction is a real, recoverable-only-with-a-backup mistake — worth checking deliberately rather than assuming the default matches your intent.
What to actually remember from this post
emptyDiris temporary scratch space, tied to the Pod's lifetime — not a durable storage solution.- PVCs are requests; PVs are the actual provisioned storage — a PVC and its underlying data survive a Pod being deleted and recreated.
- StatefulSets, not Deployments, are for workloads with real per-replica identity and attached storage — most obviously, databases.
reclaimPolicyon a StorageClass decides whether deleting a PVC destroys the underlying disk — verify it matches what you actually intend for genuinely important data.
Next in the series: Autoscaling and Resource Management in Kubernetes, the final post — covering how Kubernetes decides how much CPU and memory a Pod actually gets, and when to add more of them.
