EthicsComputerCourses
Join training +91 90532 10052

StatefulSets

Kubernetes · Lesson 17 of 30 Intermediate 6 min read Updated

Deployments treat Pods like identical, replaceable workers: random names, shared template, any one can be deleted. That is perfect for web servers and wrong for databases. A database member needs a name that does not change, its own disk that follows it, and often a fixed start order (the primary first, then replicas). A StatefulSet provides exactly that.

In this lesson you will create a three-replica StatefulSet, see its stable names, DNS records and per-Pod volumes, and learn how its scaling and updates differ from a Deployment's.

What a StatefulSet guarantees

  • Stable, ordered names. Pods are called <name>-0, <name>-1, <name>-2. If store-1 is deleted, the replacement is again store-1.
  • Stable network identity. With a headless Service, each Pod gets its own DNS name: store-1.store.default.svc.cluster.local.
  • Stable storage. volumeClaimTemplates create a separate PVC for each Pod (data-store-0, data-store-1 ...). A replacement Pod is attached to the same claim, on whatever node it lands.
  • Order. By default Pods are created one at a time, 0 then 1 then 2, each waiting for the previous one to be ready, and removed in reverse order.

A StatefulSet with per-Pod storage

This example uses Nginx so you can see each Pod's identity; each Pod writes its own host name into its own volume when it starts:

YAML
apiVersion: v1
kind: Service
metadata:
  name: store
spec:
  clusterIP: None
  selector:
    app: store
  ports:
    - name: http
      port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: store
spec:
  serviceName: store
  replicas: 3
  selector:
    matchLabels:
      app: store
  template:
    metadata:
      labels:
        app: store
    spec:
      containers:
        - name: web
          image: nginx:1.30-alpine
          command: ["sh", "-c", "echo \"Served by $(hostname)\" > /usr/share/nginx/html/index.html && exec nginx -g 'daemon off;'"]
          ports:
            - name: http
              containerPort: 80
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi
  • The Service has clusterIP: None, which makes it headless: no virtual IP, but DNS records for each Pod.
  • serviceName links the StatefulSet to that Service, which is what creates the per-Pod DNS names.
  • volumeClaimTemplates is a list of PVC templates. Each Pod gets one PVC per template, named <template>-<pod>.
Terminal
kubectl apply -f store.yaml
kubectl get statefulset store
kubectl get pods -l app=store
kubectl get pvc
Output
service/store created
statefulset.apps/store created
NAME    READY   AGE
store   3/3     70s
NAME      READY   STATUS    RESTARTS   AGE
store-0   1/1     Running   0          70s
store-1   1/1     Running   0          52s
store-2   1/1     Running   0          35s
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
data-store-0   Bound    pvc-0b6e2d41-7c3a-4f58-9e21-c4d8a6f3b7e0   1Gi        RWO            standard       <unset>                 70s
data-store-1   Bound    pvc-5a91f3c8-2e47-4b6d-8f03-1d7c9b2e6a45   1Gi        RWO            standard       <unset>                 52s
data-store-2   Bound    pvc-c37d8e05-9f12-4a6b-b5e4-7e0a2f9d1c88   1Gi        RWO            standard       <unset>                 35s

Look at the ages: store-0 started first, and each next Pod started only after the one before it was ready.

Reaching one specific Pod

Clients that need a particular member use its own DNS name, <pod>.<service>:

Terminal
kubectl run tmp --rm -i --restart=Never --image=busybox:1.37 -- wget -qO- http://store-1.store
Output
Served by store-1
pod "tmp" deleted from default namespace

A database client would connect to store-0.store for the primary, for example. Asking for the plain name store returns the addresses of all ready Pods.

Replacement keeps the identity

Delete a Pod and watch what comes back:

Terminal
kubectl delete pod store-1
kubectl get pods -l app=store
Output
pod "store-1" deleted from default namespace
NAME      READY   STATUS    RESTARTS   AGE
store-0   1/1     Running   0          4m2s
store-1   1/1     Running   0          9s
store-2   1/1     Running   0          3m27s

The new Pod has the same name, the same DNS name and the same claim, data-store-1. Its IP address is new, which is why clients use DNS names rather than IPs.

Scaling, deleting and updating

  • Scaling down from 3 to 2 removes store-2, the highest number, first. Its PVC is kept by default, so scaling back up reattaches the same data.
  • Deleting the StatefulSet also keeps the PVCs. You can change both behaviours with persistentVolumeClaimRetentionPolicy (fields whenDeleted and whenScaled, each Retain or Delete), a stable feature since Kubernetes 1.32. Keeping data by default is a deliberate safety choice.
  • Rolling updates replace Pods one at a time in reverse order: store-2, then store-1, then store-0. Setting updateStrategy.rollingUpdate.partition: 2 updates only Pods numbered 2 and above, which lets you test a new version on one member first.
  • podManagementPolicy: Parallel starts and stops all Pods at once when strict ordering is not needed. The default is OrderedReady.

A single MySQL instance

For a small app, a one-replica StatefulSet is a simple, honest setup. The main parts look like this (the project builds the full version):

YAML
spec:
  serviceName: mysql
  replicas: 1
  template:
    spec:
      containers:
        - name: mysql
          image: mysql:8.4
          env:
            - name: MYSQL_ROOT_PASSWORD_FILE
              value: /run/secrets/db/root-password
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql
            - name: db-secrets
              mountPath: /run/secrets/db
              readOnly: true
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi

Common mistakes

  • Forgetting the headless Service or using a different name in serviceName. The per-Pod DNS names do not exist.
  • Expecting replicas to share data. Each Pod has its own volume; replication must come from the application.
  • Deleting PVCs while "cleaning up". That is the data. Back it up first.
  • Using a Deployment for a database. Random names and a shared or contested disk lead to corruption or stuck Pods.

Key points

  • StatefulSets give Pods stable ordinal names, stable DNS names and their own persistent volumes.
  • A headless Service (clusterIP: None) named in serviceName creates DNS names like store-1.store.
  • volumeClaimTemplates create one PVC per Pod, kept on scale-down and deletion unless the retention policy says otherwise.
  • Pods start in order and update in reverse order; partition limits an update to higher-numbered Pods.
  • A StatefulSet does not replicate data; use a database operator or a managed database for real clusters.

Exercise

Write a headless Service and a StatefulSet called queue with 2 replicas of busybox:1.37 running sleep 3600, each with its own 500 MiB volume mounted at /var/queue. Which PVC names will be created, and what is the full DNS name of the second Pod in the default namespace?

Show solution
YAML
apiVersion: v1
kind: Service
metadata:
  name: queue
spec:
  clusterIP: None
  selector:
    app: queue
  ports:
    - port: 5672
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: queue
spec:
  serviceName: queue
  replicas: 2
  selector:
    matchLabels:
      app: queue
  template:
    metadata:
      labels:
        app: queue
    spec:
      containers:
        - name: worker
          image: busybox:1.37
          command: ["sleep", "3600"]
          volumeMounts:
            - name: messages
              mountPath: /var/queue
  volumeClaimTemplates:
    - metadata:
        name: messages
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 500Mi

The PVCs are messages-queue-0 and messages-queue-1. The second Pod is queue-1, so its full DNS name is queue-1.queue.default.svc.cluster.local. The port in the Service is only there as documentation of what a real queue would use.

Quick Quiz

1. What is the name of the PVC for Pod store-2 from a volume claim template called data?

2. You delete the Pod store-1. What does the StatefulSet create?

3. In which order does a StatefulSet roll out an update by default?

4. What does a StatefulSet NOT do for a database?

Learn Kubernetes with a mentor

Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call