Containers are ephemeral — their files vanish on restart. Persist data with volumes, PersistentVolumeClaims, and StorageClasses, then run stateful apps like databases with StatefulSets.
Why: when a container restarts, everything written inside it is gone. That is fine for stateless web apps but fatal for a database. Kubernetes separates the request for storage (a PersistentVolumeClaim) from the actual disk (a PersistentVolume), so your app asks for "10Gi" and the cluster provisions real storage to match — wherever the pod runs.
Pod ──uses──▶ PersistentVolumeClaim ──bound to──▶ PersistentVolume
(your app) ("I need 10Gi, RWO") (a real disk on a node
or in the cloud)Why: a PersistentVolumeClaim (PVC) is how a pod asks for durable storage. accessModes says how it can be mounted (ReadWriteOnce = by one node at a time), and resources.requests.storage is the size. A StorageClass decides which kind of disk backs it — most clusters provision one automatically when the claim is made.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: standardWhy: once the PVC exists, mount it like any volume. Data written under the mountPath now survives pod restarts, reschedules, and image updates — it lives on the PersistentVolume, not inside the container.
spec:
containers:
- name: db
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: data # the PVC from the previous stepWhy: a Deployment treats its pods as interchangeable. Databases are not — each needs a stable name and its own disk that follows it. A StatefulSet gives pods predictable names (db-0, db-1, …), its own PVC per pod via volumeClaimTemplates, and ordered, one-at-a-time rollouts. Use it for anything that stores data or forms a cluster (Postgres, Kafka, Elasticsearch).
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # each pod gets its OWN 10Gi disk
- metadata:
name: data
spec:
accessModes: ['ReadWriteOnce']
resources:
requests:
storage: 10Gi