In this episode, we'll discuss Kubernetes StatefulSet for managing stateful applications. We'll learn about stable network identities, persistent storage, ordered deployment, and best practices for databases and stateful workloads.

Note
If you want to read the previous episode, you can click the Episode 26 thumbnail below
In the previous episode, we learned about Deployment for managing stateless applications with rolling updates and easy scaling. In episode 27, we'll discuss StatefulSet, designed specifically for stateful applications that require stable network identities and persistent storage.
Note: Here I'll be using a Kubernetes Cluster installed through K3s.
While Deployments work great for stateless applications, stateful applications like databases, message queues, and distributed systems need guarantees about Pod identity, ordering, and storage persistence. StatefulSet provides these guarantees.
A StatefulSet is a Kubernetes workload resource that manages stateful applications, providing stable network identities, persistent storage, and ordered deployment and scaling.
Think of StatefulSet like a numbered team where each member has a specific role and identity - member-0 is always the leader, member-1 is always the backup, and so on. Unlike a Deployment where all Pods are interchangeable, StatefulSet Pods have unique, persistent identities.
Key characteristics of StatefulSet:
Understanding the key differences:
| Aspect | StatefulSet | Deployment |
|---|---|---|
| Pod Identity | Stable, unique (web-0, web-1) | Random (web-abc123) |
| Network Identity | Stable hostname | Random hostname |
| Storage | Individual PVC per Pod | Shared or no storage |
| Deployment Order | Sequential (0→1→2) | Parallel |
| Scaling Order | Sequential | Parallel |
| Use Case | Databases, stateful apps | Web servers, APIs |
| Pod Replacement | Same identity preserved | New random identity |
StatefulSet solves critical challenges for stateful applications:
Without StatefulSet, managing stateful applications would require complex custom logic for identity management, storage allocation, and ordered operations.
Let's create a basic StatefulSet.
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
name: webApply the StatefulSet:
sudo kubectl apply -f web-statefulset.ymlWatch Pods being created:
sudo kubectl get pods -w -l app=nginxOutput shows sequential creation:
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 10s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 10s
web-2 0/1 Pending 0 0s
web-2 0/1 ContainerCreating 0 0s
web-2 1/1 Running 0 10sNotice:
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1GiThis creates:
StatefulSets require a Headless Service for network identity.
A Headless Service (clusterIP: None) doesn't load balance. Instead, it returns the IP addresses of individual Pods, enabling direct Pod-to-Pod communication.
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None # Makes it headless
selector:
app: nginxEach Pod gets a predictable DNS name:
<pod-name>.<service-name>.<namespace>.svc.cluster.localExamples:
web-0.nginx.default.svc.cluster.localweb-1.nginx.default.svc.cluster.localweb-2.nginx.default.svc.cluster.localTest DNS resolution:
sudo kubectl run -it --rm debug --image=busybox:1.36 --restart=Never -- nslookup web-0.nginxScale up and down in order.
sudo kubectl scale statefulset web --replicas=5Pods created sequentially:
sudo kubectl scale statefulset web --replicas=2Pods deleted in reverse order:
Important
Important: Scaling down doesn't delete PersistentVolumeClaims. They remain for data safety and can be reused if you scale back up.
StatefulSet supports two update strategies.
Updates Pods one at a time in reverse order:
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0Partition: Only Pods with ordinal >= partition are updated.
Example with partition=2:
Pods only updated when manually deleted:
spec:
updateStrategy:
type: OnDeleteUseful for manual control over updates.
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None
selector:
app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/mysql
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10GiapiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
data:
POSTGRES_DB: "myapp"
POSTGRES_USER: "appuser"
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
type: Opaque
stringData:
POSTGRES_PASSWORD: "secretpassword"
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
ports:
- port: 5432
name: postgres
clusterIP: None
selector:
app: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
name: postgres
envFrom:
- configMapRef:
name: postgres-config
- secretRef:
name: postgres-secret
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command:
- pg_isready
- -U
- appuser
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- appuser
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20GiapiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
redis.conf: |
appendonly yes
appendfilename "appendonly.aof"
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
ports:
- port: 6379
name: redis
clusterIP: None
selector:
app: redis
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.2
ports:
- containerPort: 6379
name: redis
command:
- redis-server
- /etc/redis/redis.conf
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/redis
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: config
configMap:
name: redis-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5GiapiVersion: v1
kind: Service
metadata:
name: kafka
spec:
ports:
- port: 9092
name: kafka
clusterIP: None
selector:
app: kafka
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
spec:
serviceName: kafka
replicas: 3
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
spec:
containers:
- name: kafka
image: confluentinc/cp-kafka:latest
ports:
- containerPort: 9092
name: kafka
env:
- name: KAFKA_BROKER_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: KAFKA_ZOOKEEPER_CONNECT
value: "zookeeper:2181"
- name: KAFKA_ADVERTISED_LISTENERS
value: "PLAINTEXT://$(POD_NAME).kafka:9092"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: data
mountPath: /var/lib/kafka/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10GiControl how Pods are managed during operations.
Pods created/deleted sequentially, waiting for each to be Ready:
spec:
podManagementPolicy: OrderedReadyPods created/deleted in parallel (like Deployment):
spec:
podManagementPolicy: ParallelUseful when Pod order doesn't matter but you still need stable identities.
sudo kubectl delete statefulset web --cascade=orphanPods remain running but are no longer managed.
sudo kubectl delete statefulset webPods deleted in reverse order.
sudo kubectl delete pvc -l app=nginxWarning
Warning: Deleting PVCs permanently deletes data. Always backup before deleting.
Problem: StatefulSet requires a headless Service.
Solution: Always create headless Service first:
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
clusterIP: None # Required for StatefulSet
selector:
app: nginxProblem: serviceName doesn't match Service metadata.name.
Solution: Ensure names match:
# Service
metadata:
name: nginx
# StatefulSet
spec:
serviceName: "nginx" # Must matchProblem: PVCs can't be provisioned.
Solution: Ensure StorageClass exists or specify one:
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: fast-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10GiProblem: Data lost when scaling down.
Solution: PVCs are preserved by design. Delete manually only when certain:
# Scaling down doesn't delete PVCs
sudo kubectl scale statefulset web --replicas=1
# PVCs remain for web-1, web-2
sudo kubectl get pvcProblem: Pods can consume unlimited resources.
Solution: Always set limits for stateful workloads:
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"Choose storage based on requirements:
# Fast SSD for databases
storageClassName: fast-ssd
# Standard HDD for logs
storageClassName: standardProtect against voluntary disruptions:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: nginxPrepare environment before main container starts:
initContainers:
- name: init-config
image: busybox:1.36
command:
- sh
- -c
- |
echo "Initializing..."
# Setup configurationMonitor Pod health:
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- mysqladmin
- ping
initialDelaySeconds: 5
periodSeconds: 5Spread Pods across nodes:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- mysql
topologyKey: kubernetes.io/hostnameImplement backup strategy:
# Example: Backup MySQL
sudo kubectl exec mysql-0 -- mysqldump -u root -p$PASSWORD --all-databases > backup.sqlsudo kubectl get statefulsets
sudo kubectl get statefulsets -o widesudo kubectl describe statefulset websudo kubectl get pods -l app=nginxsudo kubectl get pvcsudo kubectl run -it --rm debug --image=busybox:1.36 --restart=Never -- nslookup web-0.nginxsudo kubectl get statefulset web
sudo kubectl describe statefulset websudo kubectl get pods -l app=nginx
sudo kubectl describe pod web-0
sudo kubectl logs web-0sudo kubectl get pvc
sudo kubectl describe pvc www-web-0sudo kubectl get service nginx
sudo kubectl describe service nginxsudo kubectl get events --sort-by='.lastTimestamp'In episode 27, we've explored StatefulSet in Kubernetes in depth. We've learned how to manage stateful applications with stable identities, persistent storage, and ordered operations.
Key takeaways:
StatefulSet is essential for running stateful applications in Kubernetes. By understanding StatefulSets, you can confidently deploy and manage databases, distributed systems, and other stateful workloads with guaranteed identity and storage persistence.
Are you getting a clearer understanding of StatefulSet in Kubernetes? Keep your learning momentum going and look forward to the next episode!
Note
If you want to continue to the next episode, you can click the Episode 28 thumbnail below