🎤Speaking at KubeCon EU 2026Lessons Learned Orchestrating Multi-Tenant GPUs on OpenShift AIView Session
Storage intermediate ⏱ 15 minutes K8s 1.28+

How to Deploy Stateful Applications

Run stateful workloads on Kubernetes with StatefulSets. Manage stable identities, persistent storage, and ordered deployment for databases and caches.

By Luca Berton

How to Deploy Stateful Applications

StatefulSets manage stateful applications that require stable network identities, persistent storage, and ordered deployment. Essential for databases, caches, and distributed systems.

StatefulSet vs Deployment

# StatefulSet provides:
# - Stable, unique network identifiers (pod-0, pod-1, pod-2)
# - Stable, persistent storage per pod
# - Ordered, graceful deployment and scaling
# - Ordered, automated rolling updates

# Use StatefulSet for:
# - Databases (MySQL, PostgreSQL, MongoDB)
# - Distributed systems (Kafka, Elasticsearch, Cassandra)
# - Caches (Redis cluster)
# - Any app needing stable identity

Basic StatefulSet

# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "web"  # Required: Headless service name
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 1Gi

Headless Service

# headless-service.yaml
# Required for StatefulSet DNS
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  clusterIP: None  # Headless service
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
# DNS records created:
# web-0.web.default.svc.cluster.local
# web-1.web.default.svc.cluster.local
# web-2.web.default.svc.cluster.local

# Test DNS resolution
kubectl run -it --rm debug --image=busybox -- nslookup web-0.web

PostgreSQL StatefulSet

# postgres-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:15
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: 2000m
              memory: 4Gi
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432

Redis Cluster

# redis-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis
  replicas: 6  # 3 masters + 3 replicas
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7
          command:
            - redis-server
          args:
            - /etc/redis/redis.conf
            - --cluster-enabled
            - "yes"
            - --cluster-config-file
            - /data/nodes.conf
          ports:
            - containerPort: 6379
              name: client
            - containerPort: 16379
              name: gossip
          volumeMounts:
            - name: data
              mountPath: /data
            - name: config
              mountPath: /etc/redis
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
      volumes:
        - name: config
          configMap:
            name: redis-config
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi

Ordered Pod Management

# Default: OrderedReady
# Pods created in order: pod-0, pod-1, pod-2
# Pods deleted in reverse: pod-2, pod-1, pod-0
# Each pod must be Running and Ready before next starts

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: ordered-app
spec:
  podManagementPolicy: OrderedReady  # Default
  # Or: Parallel - create/delete all at once
  replicas: 3
  # ...

Update Strategies

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: app
spec:
  updateStrategy:
    type: RollingUpdate  # Default
    rollingUpdate:
      partition: 0  # Update all pods
      # partition: 2  # Only update pods >= 2 (canary)
      maxUnavailable: 1  # Kubernetes 1.24+
  # ...
# Canary update with partition
# Only pods with ordinal >= partition are updated

# Set partition to 2 (only update pod-2)
kubectl patch statefulset app -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'

# Update image
kubectl set image statefulset/app nginx=nginx:1.25

# Only pod-2 gets new image
# Verify, then lower partition
kubectl patch statefulset app -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'

Scaling StatefulSets

# Scale up (pods added in order)
kubectl scale statefulset web --replicas=5

# Scale down (pods removed in reverse order)
kubectl scale statefulset web --replicas=2

# PVCs are NOT deleted when scaling down
# Manual cleanup if needed:
kubectl delete pvc data-web-3 data-web-4

Init Containers for StatefulSets

# init-container-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
        - name: init-mysql
          image: mysql:8
          command:
            - bash
            - -c
            - |
              # Generate server-id from pod ordinal
              [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
              ordinal=${BASH_REMATCH[1]}
              echo "[mysqld]" > /mnt/conf.d/server-id.cnf
              echo "server-id=$((100 + $ordinal))" >> /mnt/conf.d/server-id.cnf
              
              # Copy config based on primary/replica
              if [[ $ordinal -eq 0 ]]; then
                cp /mnt/config-map/primary.cnf /mnt/conf.d/
              else
                cp /mnt/config-map/replica.cnf /mnt/conf.d/
              fi
          volumeMounts:
            - name: conf
              mountPath: /mnt/conf.d
            - name: config-map
              mountPath: /mnt/config-map
      containers:
        - name: mysql
          image: mysql:8
          # ...

Pod Identity in Container

# Get pod ordinal from hostname
hostname  # Returns: web-0, web-1, etc.

# Extract ordinal number
ORDINAL=$(hostname | grep -oE '[0-9]+$')

# Use in application logic
if [ "$ORDINAL" == "0" ]; then
  echo "I am the primary"
else
  echo "I am replica $ORDINAL"
fi

Persistent Volume Retention

# Kubernetes 1.27+
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: app
spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain   # Keep PVCs when StatefulSet deleted
    whenScaled: Delete    # Delete PVCs when scaling down
  # Options: Retain (default) or Delete

Summary

StatefulSets provide stable identities (pod-0, pod-1), persistent storage via volumeClaimTemplates, and ordered deployment/scaling for stateful applications. Always create a headless Service for DNS-based discovery. Use init containers to configure pods based on their ordinal. Update strategies support rolling updates with partitions for canary deployments. PVCs persist by default when scaling down or deleting - configure retention policies as needed. Essential for running databases, distributed systems, and any workload requiring stable network identity.


📘 Go Further with Kubernetes Recipes

Love this recipe? There’s so much more! This is just one of 100+ hands-on recipes in our comprehensive Kubernetes Recipes book.

Inside the book, you’ll master:

  • ✅ Production-ready deployment strategies
  • ✅ Advanced networking and security patterns
  • ✅ Observability, monitoring, and troubleshooting
  • ✅ Real-world best practices from industry experts

“The practical, recipe-based approach made complex Kubernetes concepts finally click for me.”

👉 Get Your Copy Now — Start building production-grade Kubernetes skills today!

#statefulset #databases #persistence #storage #stateful

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.