StatefulSet for Stateful Apps
Learn to run a stateful application with StatefulSet in Kubernetes. Covers core concepts, hands-on steps, troubleshooting, and comparison with Deployments.
Focus: run a stateful application with StatefulSet
Stateless applications are easy to scale: you just add more replicas and route traffic to any of them. But what about databases, caches, or any application that needs to remember its data across restarts? A regular Kubernetes Deployment will happily replace a crashed Pod, but it gives each new Pod a random name and no guarantee of stable storage. That's where StatefulSets come in. They provide ordered, predictable Pods with stable identities for your stateful workloads, making them the correct choice for running databases like PostgreSQL or Redis in production.
The problem this lesson solves
When you use a Deployment to run an application like a MySQL database, every time a Pod restarts or gets rescheduled it gets a new hostname and a new volume. Clients that connected to mysql-0 cannot find it anymore because the new Pod is called something else. Worse, if a Pod moves to a different node you may lose its attached storage entirely, corrupting your data.
Stateful applications need three guarantees that Deployments do not provide:
- Stable network identity — each Pod gets a predictable hostname that survives restarts
- Stable, persistent storage — volumes are not deleted when the Pod is deleted
- Ordered, graceful deployment and scaling — Pods are created one at a time in sequence
StatefulSet is the Kubernetes workload API that delivers these guarantees.
Core concept / mental model
Think of a StatefulSet as an orchestra of named musicians. Each musician has a named seat (violin-0, violin-1, cello-0) and their own sheet music (storage). If a musician leaves, the orchestra keeps an empty chair with the same name and music ready for the next musician who sits there. Compare that to a Deployment, which is more like a bucket of identical balls — when you pull one out and replace it, you cannot tell which ball is which.
A StatefulSet maintains a sticky identity for each Pod via:
- Ordinal index — Pods are named
<statefulset-name>-<ordinal>(e.g.,my-db-0,my-db-1) - Stable network identities — each Pod gets a
StatefulSet-managed headless Service that maps each ordinal to a specific hostname - Stable storage — each Pod is bound to its own
PersistentVolumeClaim(PVC) that persists across reschedules
Pro tip: StatefulSets use a headless Service (ClusterIP: None) to let you address each Pod by its stable hostname:
my-db-0.mydb-service.default.svc.cluster.local.
How it works step by step
1. Define a headless Service
A headless Service has clusterIP: None. It creates DNS records for each Pod so you can reach it directly by name.
apiVersion: v1
kind: Service
metadata:
name: mydb
spec:
clusterIP: None
selector:
app: mydb
ports:
- name: db
port: 5432
2. Create the StatefulSet manifest
A StatefulSet looks similar to a Deployment but adds serviceName and a volumeClaimTemplates block.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mydb
spec:
serviceName: mydb
replicas: 3
selector:
matchLabels:
app: mydb
template:
metadata:
labels:
app: mydb
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi
3. Understand the creation order
Kubernetes creates Pods in order from ordinal 0 to N-1. Each Pod must be Running and Ready before the next one starts.
mydb-0is created → becomes Ready →mydb-1starts- If
mydb-0fails,mydb-1is not affected butmydb-2will not be created untilmydb-1is ready
4. Scaling down is also ordered (reverse)
When you scale down, Pods are terminated from highest to lowest ordinal, one at a time. This ensures that mydb-0 (the primary, in many database setups) is the last to be removed.
Hands-on walkthrough
Let's run a real stateful application: a 3-replica PostgreSQL cluster using a StatefulSet. You will need a Kubernetes cluster (minikube or kind works).
Step 1: Apply the headless Service and StatefulSet
Save the manifests above as mydb-service.yaml and mydb-statefulset.yaml, then apply them.
kubectl apply -f mydb-service.yaml
kubectl apply -f mydb-statefulset.yaml
Step 2: Watch Pods come up in order
kubectl get pods -w
Expected output:
NAME READY STATUS RESTARTS AGE
mydb-0 0/1 Pending 0 1s
mydb-0 1/1 Running 0 8s
mydb-1 0/1 Pending 0 0s
mydb-1 1/1 Running 0 7s
mydb-2 0/1 Pending 0 0s
mydb-2 1/1 Running 0 6s
Step 3: Verify stable network identities
kubectl run -it --rm busybox --image=busybox -- /bin/sh
Inside the pod, resolve one of the names:
nslookup mydb-0.mydb
You will see the Pod's IP and the full DNS name.
Step 4: Check persistent storage
When you delete a Pod, the PVC remains and the new Pod reuses it.
kubectl delete pod mydb-0
kubectl get pvc
Expected output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
mydb-data-mydb-0 Bound pvc-xxxxxxxxxxxx 5Gi RWO standard
Compare options / when to choose what
| Feature | Deployment | StatefulSet |
|---|---|---|
| Pod identity | Random | Fixed ordinal |
| Persistent storage | Shared volume only | Per-Pod PVC (templates) |
| Ordering on create/delete | None (parallel) | Sequential |
| Headless Service required | No | Yes |
| Use case | Web servers, APIs | Databases, message queues |
When to choose StatefulSet:
- You run a database, cache, or queue that needs a stable hostname per replica
- Each Pod requires its own persistent volume (e.g., database data directories)
- You need controlled rollouts, e.g., upgrade the primary last
When to stick with Deployment:
- Your application is stateless and can tolerate random hostnames
- You want fast, parallel scaling
- You do not need per-Pod persistent volumes
Pro tip: For most applications in Kubernetes, start with a Deployment. Only reach for a StatefulSet when you have a concrete need for stable identity or per-Pod storage — they add operational complexity.
Troubleshooting & edge cases
Pod stuck in Pending
Probably a PersistentVolumeClaim that cannot be bound. Check:
kubectl get pvc
kubectl describe pvc mydb-data-mydb-0
Make sure your cluster has a StorageClass that can provision volumes.
Pod fails to start after PVC reuse
If you delete a Pod and the new one fails with FailedMount, the old volume might still be attached to the previous node. In that case, use:
kubectl delete pvc mydb-data-mydb-0
But be careful — this deletes the data! Better to wait or force detach the volume from the previous node.
Services cannot resolve Pod hostnames
Ensure the Service is headless (clusterIP: None) and the StatefulSet's serviceName matches the Service name. Also, kube-dns or CoreDNS must be running.
Scale down with data loss
Scaling down a StatefulSet does not delete PVCs by default. You must manually delete the PVC to release the storage. This is intentional to prevent accidental data loss, but it means you must monitor orphaned PVCs.
What you learned & what's next
You now know:
- The core idea: StatefulSets give stable identity and per-Pod storage to stateful workloads
- How to run a stateful application with StatefulSet using a headless Service and
volumeClaimTemplates - Ordered operations: Pods start and stop in sequence to support database bootstrapping
- When to choose StatefulSet over Deployment for databases, caches, and queues
Next, you should explore Headless Services in depth, then PersistentVolumeClaims retention policies (whenDeleted, whenScaled) to control what happens to storage when you scale down. Then tackle rolling updates for StatefulSets — they behave differently than Deployments.
You are ready to run stateful workloads safely in Kubernetes.
Practice recap
Now, try this on your own: Modify the StatefulSet example above to run a 2-replica MongoDB replica set. Add a second container as a sidecar to initialize the replica set on first boot. Test that deleting mongo-0 and scaling down to 1 replica works without losing data.
Common mistakes
- Forgetting to create a headless Service — StatefulSet requires
clusterIP: Nonefor stable network identity. - Scaling down without cleaning up PVCs — orphaned volumes accumulate and waste storage.
- Using a Deployment for a database that needs ordered startup — parallel creation breaks cluster bootstrapping.
- Setting
replicashigher than available PersistentVolumes — Pods get stuck in Pending waiting for PVC binding.
Variations
- Use
podManagementPolicy: Parallelto allow non-ordered Pod creation for StatefulSets that do not need ordered startup (e.g., Cassandra). - Set
volumeClaimTemplateswith different StorageClass for each replica to use different types of storage (fast SSDs for primary, slower for replicas). - Combine StatefulSet with a Service that has a fixed ClusterIP for load-balanced access to the entire set, plus a headless Service for per-Pod DNS.
Real-world use cases
- Run a 3-node PostgreSQL cluster for high availability — each pod has a persistent volume, and the primary is always
pg-0for easy application configuration. - Deploy a Redis Sentinel cluster where each Redis instance needs a stable hostname and its own data directory for durability.
- Serve a Kafka message queue — each broker gets a unique ID (
kafka-0,kafka-1) that survives restarts, backed by persistent disks.
Key takeaways
- StatefulSets guarantee stable network identities via ordinal hostnames (
<name>-0,<name>-1, ...). - Each Pod gets its own PersistentVolumeClaim declared in
volumeClaimTemplates, which outlives the Pod. - Pods are created and terminated in sequential order, from ordinal 0 to N-1 (and reverse), enabling controlled database bootstrapping.
- A headless Service (
clusterIP: None) is required for DNS-based per-Pod addressing. - Do not use StatefulSet for stateless workloads — Deployments are simpler and faster.
- Scaling down does not delete PVCs — manual cleanup or retention policy is needed to avoid resource leak.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.