StatefulSets
Deployments work great for stateless applications where any Pod can replace any other. But what about stateful applications — databases, message queues, distributed systems — that need stable identities, ordered deployment, and persistent storage? That’s where StatefulSets come in.
What is a StatefulSet?
A StatefulSet manages stateful applications by providing:
- Stable, unique network identities — Pods get predictable names (e.g.,
mysql-0,mysql-1,mysql-2) - Stable, persistent storage — Each Pod gets its own PVC that follows it across rescheduling
- Ordered deployment and scaling — Pods are created sequentially (0, 1, 2…) and terminated in reverse order
- Ordered rolling updates — Updates proceed one Pod at a time, in order
StatefulSet vs. Deployment
| Feature | Deployment | StatefulSet |
|---|---|---|
| Pod names | Random (e.g., nginx-7d4f8b) |
Ordinal (e.g., mysql-0, mysql-1) |
| Pod identity | Interchangeable | Unique and sticky |
| Storage | Shared PVC (optional) | Each Pod gets its own PVC |
| Scaling order | All at once (parallel) | Sequential (0→1→2) |
| Deletion order | All at once | Reverse order (2→1→0) |
| Best for | Stateless apps (web servers, APIs) | Stateful apps (databases, queues) |
StatefulSet YAML
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # Required: headless Service name
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates: # Each Pod gets its own PVC
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Key Differences from Deployment YAML
serviceName— Required. Must reference a headless Service.volumeClaimTemplates— Instead of a shared PVC, each Pod gets its own automatically.
Headless Service
StatefulSets require a headless Service (with clusterIP: None) for stable DNS entries:
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None # Makes it headless
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
This creates DNS records for each Pod:
mysql-0.mysql-headless.default.svc.cluster.local
mysql-1.mysql-headless.default.svc.cluster.local
mysql-2.mysql-headless.default.svc.cluster.local
Now other applications can address specific Pods by name — essential for database replication where you need to know which is the primary and which are replicas.
Pod Identity and Naming
Each Pod in a StatefulSet gets a stable, predictable name:
<statefulset-name>-<ordinal>
mysql-0 ← First Pod (ordinal 0)
mysql-1 ← Second Pod (ordinal 1)
mysql-2 ← Third Pod (ordinal 2)
This identity is maintained even if the Pod is rescheduled to a different node. Pod mysql-0 will always be mysql-0.
Ordered Operations
Creation Order
mysql-0 created and Running ✓
mysql-1 created and Running ✓ (only after mysql-0 is Ready)
mysql-2 created and Running ✓ (only after mysql-1 is Ready)
Deletion Order (Reverse)
mysql-2 terminated ✓
mysql-1 terminated ✓ (only after mysql-2 is fully terminated)
mysql-0 terminated ✓ (only after mysql-1 is fully terminated)
Scale Down
# Scale from 3 to 1
kubectl scale statefulset mysql --replicas=1
# Terminates mysql-2 first, then mysql-1
# mysql-0 remains
# PVCs for mysql-1 and mysql-2 are NOT deleted (data preserved)
Persistent Storage
The volumeClaimTemplates field creates a unique PVC for each Pod:
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
mysql-data-mysql-0 Bound pv-abc 10Gi RWO
mysql-data-mysql-1 Bound pv-def 10Gi RWO
mysql-data-mysql-2 Bound pv-ghi 10Gi RWO
Key behavior:
- PVCs are named
<volumeClaimTemplate-name>-<statefulset-name>-<ordinal> - PVCs are NOT deleted when the StatefulSet is scaled down or deleted
- When scaling back up, the same PVCs are reattached to the same ordinal Pods
- Data is preserved across Pod restarts and rescheduling
Hands-On: Simple StatefulSet with Minikube
# statefulset-demo.yaml
apiVersion: v1
kind: Service
metadata:
name: web-headless
spec:
clusterIP: None
selector:
app: web
ports:
- port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: web-headless
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Mi
# Apply
kubectl apply -f statefulset-demo.yaml
# Watch Pods being created in order
kubectl get pods -w -l app=web
# Notice the ordinal naming
kubectl get pods -l app=web
# web-0 1/1 Running
# web-1 1/1 Running
# web-2 1/1 Running
# Write unique content to each Pod
for i in 0 1 2; do
kubectl exec web-$i -- sh -c "echo 'Hello from web-$i' > /usr/share/nginx/html/index.html"
done
# Delete a Pod and verify data persists
kubectl delete pod web-1
kubectl exec web-1 -- cat /usr/share/nginx/html/index.html
# Output: Hello from web-1 (data survived!)
# Test DNS resolution from another Pod
kubectl run dns-test --image=busybox --rm -it --restart=Never -- \
nslookup web-0.web-headless.default.svc.cluster.local
Update Strategies
spec:
updateStrategy:
type: RollingUpdate # Default
rollingUpdate:
partition: 0 # Only update Pods with ordinal >= partition
| Strategy | Behavior |
|---|---|
| RollingUpdate (default) | Updates Pods in reverse ordinal order (2→1→0) |
| OnDelete | Only updates a Pod when you manually delete it |
Canary Updates with Partition
# Only update Pods with ordinal >= 2 (just web-2)
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2
This lets you test the new version on web-2 before rolling it out to the rest.
When to Use StatefulSets
Use StatefulSets for:
- Databases (MySQL, PostgreSQL, MongoDB)
- Message queues (Kafka, RabbitMQ)
- Distributed systems (Elasticsearch, ZooKeeper)
- Any application requiring stable identity or persistent per-Pod storage
Use Deployments for:
- Web servers, API servers
- Stateless microservices
- Anything that doesn’t need stable identity
Summary
- StatefulSets manage stateful applications with stable identities
- Pods get predictable names (
app-0,app-1,app-2) - Each Pod gets its own PVC via
volumeClaimTemplates - Operations are ordered (creation, deletion, updates)
- Requires a headless Service for DNS-based identity
- PVCs are preserved when Pods are deleted or scaled down
What’s Next?
Now let’s explore two more workload controllers: DaemonSets (run a Pod on every node) and Jobs (run tasks to completion). These fill important gaps that Deployments and StatefulSets don’t address.