StatefulSets: apps with identity and data
A Deployment treats its Pods like identical cups from a stack: take any one, throw away a broken one, grab a new one. That works for posts and comments. A database is different. Each copy has its own data and its own job (primary, replica), so each one needs a fixed name and its own disk. A StatefulSet gives Pods exactly that.
Why a Deployment is not enough
Try running PostgreSQL with 3 replicas in a Deployment and three things go wrong:
Random names
Pods are called db-7d9f8-x2kqp, and a replacement gets a new random name. Replicas need to say "I follow db-0", and that name must not change.
One shared disk
All replicas share one PVC (chapter 31). Three databases writing the same files corrupt each other. Each needs its own disk.
All start at once
Replicas must start after the primary exists, so they have something to copy from.
Any Pod may be killed
When scaling down, a Deployment may remove the primary. You want the newest replica to go first.
The four promises
- Stable name: Pods are
db-0,db-1,db-2. A replacement for db-1 is called db-1 again. - Stable network address: with a headless Service, each Pod gets its own DNS name,
db-1.db-headless(chapter 26). - Its own disk, forever: each Pod gets its own PVC,
data-db-1, and always gets that same one back, even on another node. - Order: started 0, 1, 2 one after another; stopped 2, 1, 0.
Try it: scale, delete, scale again
Scale up and watch the Pods start one by one. Delete a Pod and see it come back with the same name and disk. Scale down, then up again, and watch the old disks get reused. Then switch to Parallel and compare.
The YAML
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db-headless
replicas: 3
podManagementPolicy: OrderedReady
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:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
persistentVolumeClaimRetentionPolicy:
whenScaled: Retain
whenDeleted: Retain
serviceName
The headless Service (clusterIP: None) that gives each Pod its own DNS name. You create it yourself, next to the StatefulSet.
podManagementPolicy
OrderedReady (default): one at a time, each waits until the previous one is Ready. Parallel: all at once, for apps that do not care about order.
volumeClaimTemplates
A PVC recipe. For each Pod, Kubernetes creates a PVC named <template>-<pod>: data-db-0, data-db-1… using the default StorageClass here (chapter 32).
persistentVolumeClaimRetentionPolicy
What happens to those PVCs when you scale down or delete the StatefulSet. Retain (default) keeps them, so the data survives. Delete removes them, and with them usually the disk.
$ kubectl get pods,pvc -l app=db
NAME READY STATUS
pod/db-0 1/1 Running
pod/db-1 1/1 Running
pod/db-2 1/1 Running
NAME STATUS CAPACITY
persistentvolumeclaim/data-db-0 Bound 10Gi
persistentvolumeclaim/data-db-1 Bound 10Gi
persistentvolumeclaim/data-db-2 Bound 10Gi
Order of start and stop
db-0 Pending → Running → Ready ✓Only db-0 starts. It becomes the primary.
db-0 Ready db-1 Pending → Running → Ready ✓db-1 starts only after db-0 is Ready, so it can connect to
db-0.db-headlessand copy its data.db-0 Ready db-1 Ready db-2 Pending → Running → Ready ✓Then db-2. If db-1 never became Ready, db-2 would wait forever, which is a feature: you notice the problem before it spreads.
db-2 TerminatingThe highest number goes first, the newest replica.
db-1 Terminating db-0 Running ← the primary is never touched firstThen db-1. db-0, the oldest and usually the primary, stays.
Updates and staged rollouts
Change the image and apply. A StatefulSet updates from the highest number down: db-2, then db-1, then db-0, each only after the previous is Ready again. That way the primary is updated last.
To test a new version on just a few Pods first, use a partition: only Pods with a number greater than or equal to the partition get the new version.
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2
db-0 postgres:16 (old)
db-1 postgres:16 (old)
db-2 postgres:16.4 ← only this oneA canary: one replica runs the new version. Watch it for a while.
db-0 postgres:16 (old)
db-1 postgres:16.4 ← new
db-2 postgres:16.4 ← newLooks good: lower the partition to roll one step further.
db-0 postgres:16.4
db-1 postgres:16.4
db-2 postgres:16.4Partition 0 is the default: everyone is updated.
Scaling down safely
- Disks stay behind (with the default
Retain). Scale back up and db-2 getsdata-db-2again, data included. Disks you no longer want must be deleted by hand:kubectl delete pvc data-db-2. - The app must agree. Kubernetes does not move data. Before removing db-2, the database must stop counting on it (leave the cluster, hand over its data). Otherwise you may lose data or break a quorum.
- A stuck node blocks you. If the node running db-1 disappears, Kubernetes will not start a second db-1 elsewhere until it is sure the first one is gone. Two db-1 Pods writing the same data would be worse than waiting.
What a StatefulSet does not do
A StatefulSet gives names, disks and order. It knows nothing about databases. It does not:
- copy data from the primary to replicas,
- pick a new primary when db-0 dies,
- take backups, or resize the cluster safely.
The database does the first two itself, and the rest is your job, or an operator's: a controller that knows how to run one specific database (chapter 46). In practice, most people install PostgreSQL, MySQL or Kafka with a Helm chart or an operator, which creates the StatefulSet for them. Or they use a managed cloud database and keep the cluster stateless.
StatefulSet or Deployment?
- A 3-node Kafka cluster
Each broker has its own id, its own data and peers that find it by name.
- The query service, which rebuilds its data from events
Any copy can answer and nothing must survive on its disk. Stateless: Deployment.
- PostgreSQL with one primary and two replicas
The classic case: stable names, a disk each, primary started first.
Recap
- StatefulSet Pods have fixed names (
db-0,db-1…) that survive replacement. - A headless Service gives each Pod a fixed DNS name.
volumeClaimTemplatesgive each Pod its own PVC, which follows it and is kept on scale-down.- Pods start in order 0, 1, 2 and stop in reverse;
Parallelturns that off. - Updates go from the highest number down; a
partitionlets you roll out step by step. - Replication, failover and backups are the database's or an operator's job, not the StatefulSet's.
Quick check
db from 3 to 2, and a week later back to 3. Does db-2 start empty?Show answer
No. Its PVC data-db-2 was kept (retention policy Retain), so the new db-2 gets the old disk back with last week's data. The database must be able to catch up from there, or you delete the PVC before scaling up.
StatefulSets: common questions
Are StatefulSet volumes deleted when I scale down?
Not by default. The PVCs are kept, and a Pod with the same number gets its old disk back when you scale up again. persistentVolumeClaimRetentionPolicy can change this.