StatefulSets

4 minute read

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

  1. serviceName — Required. Must reference a headless Service.
  2. 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.

Next: DaemonSets and Jobs →

Updated: