StatefulSets
Deployments treat Pods like identical, replaceable workers: random names, shared template, any one can be deleted. That is perfect for web servers and wrong for databases. A database member needs a name that does not change, its own disk that follows it, and often a fixed start order (the primary first, then replicas). A StatefulSet provides exactly that.
In this lesson you will create a three-replica StatefulSet, see its stable names, DNS records and per-Pod volumes, and learn how its scaling and updates differ from a Deployment's.
What a StatefulSet guarantees
- Stable, ordered names. Pods are called
<name>-0,<name>-1,<name>-2. Ifstore-1is deleted, the replacement is againstore-1. - Stable network identity. With a headless Service, each Pod gets its own DNS name:
store-1.store.default.svc.cluster.local. - Stable storage.
volumeClaimTemplatescreate a separate PVC for each Pod (data-store-0,data-store-1...). A replacement Pod is attached to the same claim, on whatever node it lands. - Order. By default Pods are created one at a time, 0 then 1 then 2, each waiting for the previous one to be ready, and removed in reverse order.
A StatefulSet with per-Pod storage
This example uses Nginx so you can see each Pod's identity; each Pod writes its own host name into its own volume when it starts:
apiVersion: v1
kind: Service
metadata:
name: store
spec:
clusterIP: None
selector:
app: store
ports:
- name: http
port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: store
spec:
serviceName: store
replicas: 3
selector:
matchLabels:
app: store
template:
metadata:
labels:
app: store
spec:
containers:
- name: web
image: nginx:1.30-alpine
command: ["sh", "-c", "echo \"Served by $(hostname)\" > /usr/share/nginx/html/index.html && exec nginx -g 'daemon off;'"]
ports:
- name: http
containerPort: 80
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi- The Service has
clusterIP: None, which makes it headless: no virtual IP, but DNS records for each Pod. serviceNamelinks the StatefulSet to that Service, which is what creates the per-Pod DNS names.volumeClaimTemplatesis a list of PVC templates. Each Pod gets one PVC per template, named<template>-<pod>.
kubectl apply -f store.yaml
kubectl get statefulset store
kubectl get pods -l app=store
kubectl get pvcservice/store created statefulset.apps/store created NAME READY AGE store 3/3 70s NAME READY STATUS RESTARTS AGE store-0 1/1 Running 0 70s store-1 1/1 Running 0 52s store-2 1/1 Running 0 35s NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE data-store-0 Bound pvc-0b6e2d41-7c3a-4f58-9e21-c4d8a6f3b7e0 1Gi RWO standard <unset> 70s data-store-1 Bound pvc-5a91f3c8-2e47-4b6d-8f03-1d7c9b2e6a45 1Gi RWO standard <unset> 52s data-store-2 Bound pvc-c37d8e05-9f12-4a6b-b5e4-7e0a2f9d1c88 1Gi RWO standard <unset> 35s
Look at the ages: store-0 started first, and each next Pod started only after the one before it was ready.
Reaching one specific Pod
Clients that need a particular member use its own DNS name, <pod>.<service>:
kubectl run tmp --rm -i --restart=Never --image=busybox:1.37 -- wget -qO- http://store-1.storeServed by store-1 pod "tmp" deleted from default namespace
A database client would connect to store-0.store for the primary, for example. Asking for the plain name store returns the addresses of all ready Pods.
Replacement keeps the identity
Delete a Pod and watch what comes back:
kubectl delete pod store-1
kubectl get pods -l app=storepod "store-1" deleted from default namespace NAME READY STATUS RESTARTS AGE store-0 1/1 Running 0 4m2s store-1 1/1 Running 0 9s store-2 1/1 Running 0 3m27s
The new Pod has the same name, the same DNS name and the same claim, data-store-1. Its IP address is new, which is why clients use DNS names rather than IPs.
Scaling, deleting and updating
- Scaling down from 3 to 2 removes
store-2, the highest number, first. Its PVC is kept by default, so scaling back up reattaches the same data. - Deleting the StatefulSet also keeps the PVCs. You can change both behaviours with
persistentVolumeClaimRetentionPolicy(fieldswhenDeletedandwhenScaled, eachRetainorDelete), a stable feature since Kubernetes 1.32. Keeping data by default is a deliberate safety choice. - Rolling updates replace Pods one at a time in reverse order:
store-2, thenstore-1, thenstore-0. SettingupdateStrategy.rollingUpdate.partition: 2updates only Pods numbered 2 and above, which lets you test a new version on one member first. podManagementPolicy: Parallelstarts and stops all Pods at once when strict ordering is not needed. The default isOrderedReady.
A single MySQL instance
For a small app, a one-replica StatefulSet is a simple, honest setup. The main parts look like this (the project builds the full version):
spec:
serviceName: mysql
replicas: 1
template:
spec:
containers:
- name: mysql
image: mysql:8.4
env:
- name: MYSQL_ROOT_PASSWORD_FILE
value: /run/secrets/db/root-password
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: db-secrets
mountPath: /run/secrets/db
readOnly: true
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5GiCommon mistakes
- Forgetting the headless Service or using a different name in
serviceName. The per-Pod DNS names do not exist. - Expecting replicas to share data. Each Pod has its own volume; replication must come from the application.
- Deleting PVCs while "cleaning up". That is the data. Back it up first.
- Using a Deployment for a database. Random names and a shared or contested disk lead to corruption or stuck Pods.
Key points
- StatefulSets give Pods stable ordinal names, stable DNS names and their own persistent volumes.
- A headless Service (
clusterIP: None) named inserviceNamecreates DNS names likestore-1.store. volumeClaimTemplatescreate one PVC per Pod, kept on scale-down and deletion unless the retention policy says otherwise.- Pods start in order and update in reverse order;
partitionlimits an update to higher-numbered Pods. - A StatefulSet does not replicate data; use a database operator or a managed database for real clusters.
Exercise
Write a headless Service and a StatefulSet called queue with 2 replicas of busybox:1.37 running sleep 3600, each with its own 500 MiB volume mounted at /var/queue. Which PVC names will be created, and what is the full DNS name of the second Pod in the default namespace?
Show solution
apiVersion: v1
kind: Service
metadata:
name: queue
spec:
clusterIP: None
selector:
app: queue
ports:
- port: 5672
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: queue
spec:
serviceName: queue
replicas: 2
selector:
matchLabels:
app: queue
template:
metadata:
labels:
app: queue
spec:
containers:
- name: worker
image: busybox:1.37
command: ["sleep", "3600"]
volumeMounts:
- name: messages
mountPath: /var/queue
volumeClaimTemplates:
- metadata:
name: messages
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 500MiThe PVCs are messages-queue-0 and messages-queue-1. The second Pod is queue-1, so its full DNS name is queue-1.queue.default.svc.cluster.local. The port in the Service is only there as documentation of what a real queue would use.
Quick Quiz
1. What is the name of the PVC for Pod store-2 from a volume claim template called data?
The pattern is <template name>-<pod name>.
2. You delete the Pod store-1. What does the StatefulSet create?
Identity and storage are kept; only the IP address changes.
3. In which order does a StatefulSet roll out an update by default?
Reverse ordinal order, waiting for each Pod to be ready.
4. What does a StatefulSet NOT do for a database?
Replication and failover come from the database itself or an operator.
Learn Kubernetes with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
