| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; any Kubernetes cluster |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | StatefulSet and headless Service YAML, stable identity, volumeClaimTemplates, ordered scale and rollout, PVC retention policy, DNS verification, and troubleshooting. Does not cover storage provisioner install, database clustering, backup, probes, or full Services tutorial. |
| Related guides | Kubernetes volumes Choose a Kubernetes workload resource |
This walkthrough uses one StatefulSet named database with three replicas in the sts-lab namespace. You need a cluster StorageClass that supports dynamic volume provisioning. The lab uses local-path when that StorageClass is present.
What Is a Kubernetes StatefulSet?
A StatefulSet manages Pods that require stable identity. Unlike a Deployment, each replica is distinguishable by name and ordinal rather than treated as fully interchangeable.
- Each Pod receives a predictable name such as
database-0,database-1,database-2 - Pods can retain persistent storage across recreation when
volumeClaimTemplatesis configured - With the default
OrderedReadypolicy, StatefulSet Pods are created and scaled in ordinal order, and rolling updates normally proceed from the highest ordinal downward - Deleting the StatefulSet object directly does not guarantee ordered, graceful Pod termination; scale it to zero first when ordered shutdown is required
A StatefulSet normally pairs with a headless Service so each Pod has a stable DNS hostname. The StatefulSet manages the Pods. The governing headless Service provides their network identity, while volumeClaimTemplates creates ordinal-associated PVCs when configured.
When to use a StatefulSet
Reach for a StatefulSet when the application needs stable ordinal identity, predictable per-Pod DNS, ordered lifecycle, per-replica storage, or a combination of these properties.
- databases and distributed data stores
- message brokers with per-node data directories
- cluster members that must be addressed by predictable names
- applications that require one persistent volume per replica
Using persistent storage alone does not always require a StatefulSet. A single Pod with a PVC, or a Deployment with shared storage when the backend supports concurrent access, may be enough. A StatefulSet does not require volumeClaimTemplates; per-replica claims are optional. Direct StatefulSet deletion does not guarantee ordered Pod termination.
Use a Deployment when replicas are interchangeable. Use a StatefulSet when replicas need stable identity, per-ordinal storage, or ordered lifecycle. See Deployment versus StatefulSet for the complete decision guide.
Create a StatefulSet Lab
Check the StorageClass
Before you save the StatefulSet manifest, confirm that a StorageClass exists for dynamic provisioning:
kubectl get storageclassSample output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 2dUse the exact name shown by your cluster. If no StorageClass is available, the PVCs will remain Pending until a provisioner or suitable pre-created volumes are available. StatefulSet storage must be dynamically provisioned through the requested StorageClass or provided by an administrator.
Create the headless Service
StatefulSets commonly pair with a headless Service because:
- it does not assign a single cluster virtual IP for load balancing across Pods
- it creates DNS records for individual Pod endpoints
- it gives each Pod a predictable network identity through
serviceName
Save this manifest as database-headless.yaml:
apiVersion: v1
kind: Service
metadata:
name: database
namespace: sts-lab
spec:
clusterIP: None
selector:
app: database
ports:
- port: 80
name: webCreate the namespace, then apply the Service:
kubectl create namespace sts-labSample output:
namespace/sts-lab createdkubectl apply -f database-headless.yamlSample output:
service/database createdFor ClusterIP, NodePort, and LoadBalancer behaviour, see Kubernetes Services. This article only needs the headless pattern.
Create the StatefulSet
Save the StatefulSet manifest as database-sts.yaml. It references the headless Service through serviceName, declares three replicas, and provisions one PVC per Pod through volumeClaimTemplates:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: database
namespace: sts-lab
spec:
serviceName: database
replicas: 3
selector:
matchLabels:
app: database
template:
metadata:
labels:
app: database
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
volumeMounts:
- name: data
mountPath: /var/lib/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 1GiReplace storageClassName with a StorageClass available in your cluster. Omit the field only when a default StorageClass exists.
Apply the StatefulSet:
kubectl apply -f database-sts.yamlSample output:
statefulset.apps/database createdVerify Pods and PVCs
Watch ordinal creation:
kubectl get pods -n sts-lab --watchObserve database-0, then database-1, and finally database-2. Press Ctrl+C after all three reach Running.
Wait before you trust the StatefulSet status columns:
kubectl wait --for=jsonpath='{.status.readyReplicas}'=3 statefulset/database -n sts-lab --timeout=180sSample output:
statefulset.apps/database condition metUnder OrderedReady, the controller creates Pods in increasing ordinal order and waits for each preceding Pod to become Ready.
kubectl get statefulsets -n sts-labSample output:
NAME READY AGE
database 3/3 21sList the claims the StatefulSet created:
kubectl get pvc -n sts-labSample output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-database-0 Bound pvc-1a67f90f-f0b7-40f9-be08-d24e0bde673d 1Gi RWO local-path 21s
data-database-1 Bound pvc-a53958cf-6e37-4806-bb66-7aa5b7efd0e4 1Gi RWO local-path 14s
data-database-2 Bound pvc-c3eb88cd-afe4-4536-bab3-3c1047951742 1Gi RWO local-path 7sEach claim name follows <claimTemplate>-<statefulset>-<ordinal>.
Understand StatefulSet Identity and YAML
| Field | Rule |
|---|---|
spec.serviceName |
Governing Service name used for Pod network identity |
spec.replicas |
Desired ordinal count; defaults to one if omitted |
spec.selector |
Required, must match template labels, and is immutable |
spec.template.spec.restartPolicy |
Must be Always or omitted |
spec.volumeClaimTemplates |
Creates ordinal-associated claims and cannot be updated on an existing StatefulSet |
spec.podManagementPolicy |
OrderedReady by default; Parallel changes scaling behavior |
Stable Pod names and ordinals
Three properties define StatefulSet identity:
- Stable Pod name —
database-0keeps the same name after recreation - Stable DNS hostname —
database-0.databasewithin the namespace when the governing headless Service is configured - Stable persistent storage — ordinal
0reattaches todata-database-0whenvolumeClaimTemplatesis configured
A recreated Pod keeps the same StatefulSet ordinal but receives a new UID and may receive a new IP address. Clients should target DNS names and PVCs, not Pod IPs.
Governing Service and DNS
serviceName must match the headless Service that provides stable network identity for Pods. Kubernetes uses it when forming per-Pod DNS names. If serviceName points to a missing Service or a Service whose selector does not match Pod labels, Pod DNS records will not resolve as expected.
Within the cluster, the fully qualified name follows:
<pod-name>.<service-name>.<namespace>.svc.cluster.local
On clusters using the default cluster.local domain, the complete name is <pod>.<service>.<namespace>.svc.cluster.local. Clusters can use a different DNS domain.
For this lab:
database-0.database.sts-lab.svc.cluster.localdatabase-1.database.sts-lab.svc.cluster.local
Short names resolve inside the same namespace:
database-0.databasedatabase-1.database
Headless Services return the endpoint addresses of individual selected Pods rather than providing a load-balanced virtual IP.
By default, Service DNS normally publishes ready endpoints. Applications that require peer discovery before readiness may need publishNotReadyAddresses: true on the headless Service. That field is intended primarily for StatefulSet peer discovery through headless Services.
Per-replica volume claims
Kubernetes creates one PersistentVolumeClaim per Pod ordinal from volumeClaimTemplates. For the manifest above, claims are named:
data-database-0data-database-1data-database-2
Each Pod binds to its own claim for the life of that ordinal. volumeMounts attach each Pod's claim at the same container path. Every replica sees /var/lib/data, but the backing volume is unique per ordinal.
Selector and template rules
spec.selector.matchLabels must match spec.template.metadata.labels. Additional template labels are allowed. A mismatch is rejected at apply time.
Copy the working manifest and change only the template label to test a mismatch:
cp database-sts.yaml bad-database-sts.yamlEdit bad-database-sts.yaml: set metadata.name to bad-db and change the Pod template labels:
template:
metadata:
labels:
app: dbA server dry-run surfaces the mismatch before you apply:
kubectl apply --dry-run=server -f bad-database-sts.yamlSample output:
The StatefulSet "bad-db" is invalid: spec.template.metadata.labels: Invalid value: {"app":"db"}: `selector` does not match template `labels`StatefulSet selectors are required and immutable in apps/v1.
Verify DNS and Persistent Storage
Resolve individual Pods
Create a temporary client Pod:
kubectl run dns-test --image=busybox:1.36 --restart=Never --namespace=sts-lab --command -- sleep 3600Sample output:
pod/dns-test createdkubectl wait --for=condition=Ready pod/dns-test -n sts-lab --timeout=60sResolve an individual StatefulSet Pod:
kubectl exec dns-test -n sts-lab -- nslookup database-0.databaseSample output (trimmed):
Name: database-0.database.sts-lab.svc.cluster.local
Address: 10.244.1.25IP addresses vary between clusters. For CoreDNS troubleshooting, see Kubernetes DNS troubleshooting.
Recreate a Pod and verify its data
Write test data into the mounted volume:
kubectl exec database-0 -n sts-lab -- sh -c "echo pod0-data > /var/lib/data/test.txt && cat /var/lib/data/test.txt"Sample output:
pod0-dataOptionally record the UID before deletion:
kubectl get pod database-0 -n sts-lab -o custom-columns='NAME:.metadata.name,UID:.metadata.uid'Sample output:
NAME UID
database-0 add76b1c-3038-4d61-8352-e9b739afbe8cDelete the Pod:
kubectl delete pod database-0 -n sts-labSample output:
pod "database-0" deletedWait for the replacement Pod to be created and Ready. In Kubernetes 1.36, kubectl wait can wait for resource creation and a status condition in the same command:
kubectl wait --for=create --for=condition=Ready pod/database-0 -n sts-lab --timeout=180skubectl get pod database-0 -n sts-lab -o custom-columns='NAME:.metadata.name,UID:.metadata.uid'Sample output:
NAME UID
database-0 4500923e-7fce-47b5-86ca-e7c3236ad68fThe name stayed database-0, but the UID changed.
Read the file again:
kubectl exec database-0 -n sts-lab -- cat /var/lib/data/test.txtSample output:
pod0-dataThe Pod object is new, but it reattached to data-database-0, so the file persisted across recreation.
Scale StatefulSet Pods
Scale up to four replicas:
kubectl scale statefulset database -n sts-lab --replicas=4Sample output:
statefulset.apps/database scaledWait before you read four Ready replicas:
kubectl wait --for=jsonpath='{.status.readyReplicas}'=4 statefulset/database -n sts-lab --timeout=180skubectl get pods -n sts-lab -l app=databaseSample output:
NAME READY STATUS RESTARTS AGE
database-0 1/1 Running 0 10s
database-1 1/1 Running 0 28s
database-2 1/1 Running 0 21s
database-3 1/1 Running 0 7sScale down to two:
kubectl scale statefulset database -n sts-lab --replicas=2Sample output:
statefulset.apps/database scaledkubectl wait --for=jsonpath='{.status.readyReplicas}'=2 statefulset/database -n sts-lab --timeout=180skubectl get pods -n sts-lab -l app=databaseSample output:
NAME READY STATUS RESTARTS AGE
database-0 1/1 Running 0 14s
database-1 1/1 Running 0 32sScaling down does not delete data-database-2 or data-database-3 under the default PVC-retention policy. Scaling back up can reuse those claims for the same ordinals.
kubectl get pvc -n sts-labYou can also change spec.replicas in database-sts.yaml and re-apply for a declarative scale.
kubectl scale changes the live StatefulSet but does not update database-sts.yaml. The file still declares replicas: 3; applying it later restores three replicas. StatefulSet scaling uses ordinal order under OrderedReady, and the application should be healthy before scale-down.
OrderedReady behaviour
The default podManagementPolicy is OrderedReady:
- Pods are created in ascending ordinal order (
0, then1, then2) - the previous Pod must become Ready before the next Pod starts
- scale-down and rolling updates normally proceed from the highest ordinal downward
Parallel Pod management
podManagementPolicy: Parallel relaxes ordering for scale-up and scale-down. It does not by itself change the selected updateStrategy or turn a rolling update into an unordered update. The Parallel policy changes scaling behavior while preserving stable identity.
PVC behavior during scale-down
Under the default retention policy, PVCs for removed ordinals remain after scale-down. Ordinals that scale back up rebind to the same claim names when those claims still exist.
Update a StatefulSet
RollingUpdate and OnDelete
Change the container image on the running StatefulSet:
kubectl set image statefulset/database nginx=nginx:1.28-alpine -n sts-labSample output:
statefulset.apps/database image updatedWait for the rollout to finish:
kubectl rollout status statefulset/database -n sts-labSample output:
partitioned roll out complete: 2 new pods have been updated...Only after rollout completes, verify the image on running Pods:
kubectl get pods -n sts-lab -l app=database -o custom-columns="NAME:.metadata.name,IMAGE:.spec.containers[0].image"Sample output:
NAME IMAGE
database-0 nginx:1.28-alpine
database-1 nginx:1.28-alpinekubectl set image updates the live StatefulSet but not database-sts.yaml. Reapplying the unchanged file later can restore the old image and the file's declared replica count. In a declarative workflow, update the manifest instead.
RollingUpdate is the default strategy. OnDelete changes the Pod template in the StatefulSet object but waits for you to delete each Pod manually before the new spec takes effect.
Partitioned rolling updates
spec.updateStrategy.rollingUpdate.partition limits automated updates to Pods with ordinal greater than or equal to the partition. Ordinals below the partition keep the old template until you lower the partition.
Pods with ordinals below the partition remain on the previous revision. Even if one of those lower-ordinal Pods is deleted, the controller recreates it from the previous revision until the partition is lowered. That is an important behavior of partitioned StatefulSet rollouts. General rollout concepts also appear in Deployments and rolling updates.
Delete a StatefulSet and Manage PVCs
Default retention
Deleting the StatefulSet removes its Pods:
kubectl delete statefulset database -n sts-labSample output:
statefulset.apps "database" deletedWait for its Pods to disappear:
kubectl wait --for=delete pod -l app=database -n sts-lab --timeout=180sPVCs created from volumeClaimTemplates remain by default:
kubectl get pvc -n sts-labSample output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-database-0 Bound pvc-1a67f90f-f0b7-40f9-be08-d24e0bde673d 1Gi RWO local-path 49s
data-database-1 Bound pvc-a53958cf-6e37-4806-bb66-7aa5b7efd0e4 1Gi RWO local-path 42s
data-database-2 Bound pvc-c3eb88cd-afe4-4536-bab3-3c1047951742 1Gi RWO local-path 35s
data-database-3 Bound pvc-8c54fe49-3329-434e-9f8c-1f00c61976cd 1Gi RWO local-path 21sPersistentVolumeClaim retention policy
You can configure retention explicitly:
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain| Field | Retain |
Delete |
|---|---|---|
whenDeleted |
Keep claims when the StatefulSet is deleted | Delete claims when the StatefulSet is deleted |
whenScaled |
Keep claims for removed ordinals | Delete claims belonging to scaled-down ordinals |
The default is Retain for both.
Delete lab claims when you intend to discard application data. In this dedicated lab namespace:
kubectl delete pvc --all -n sts-labDepending on your StorageClass reclaim policy, deleting a claim may permanently remove the underlying volume. Confirm your storage policy before deleting production data. See PersistentVolume and PVC for reclaim policy detail.
Ordered shutdown considerations
Direct StatefulSet deletion starts Pod termination but does not guarantee reverse-ordinal, graceful shutdown. Scale to zero before deletion when application shutdown order matters.
Troubleshoot Common StatefulSet Problems
Start with describe output and Pod status:
kubectl describe statefulset <name> -n <namespace>
kubectl describe pod <pod> -n <namespace>| Symptom | Likely cause | First check |
|---|---|---|
| Only one Pod appears at first | Default OrderedReady policy |
Wait for previous ordinal Ready |
| Next ordinal never starts | Previous Pod not Ready | kubectl describe pod database-N |
| PVCs remain after StatefulSet delete | Default Retain policy or explicit retention policy |
kubectl get pvc and persistentVolumeClaimRetentionPolicy |
| DNS name fails | Headless Service, serviceName, or readiness timing |
Service selector, serviceName, and Pod Ready state |
| Pod Pending | Unbound PVC, missing StorageClass, or scheduling block | kubectl describe pod Events and kubectl get pvc |
| Template change ignored on running Pods | OnDelete strategy |
spec.updateStrategy.type |
| Selector rejected on apply | Selector and template labels differ | Server-side dry run |
For general Pending and ContainerCreating diagnosis, see Kubernetes Pods and Pod Lifecycle.
What's Next
References
- StatefulSets
- StatefulSet API reference
- Scaling a StatefulSet
- Deleting a StatefulSet
- Headless Services
- Persistent Volume Claims
Summary
A Kubernetes StatefulSet runs Pods that need stable names, predictable DNS hostnames through a governing headless Service, and optional per-ordinal persistent storage through volumeClaimTemplates. You set serviceName, declare replicas, and verify ordered creation under OrderedReady before trusting READY counts. The persistence test proves that ordinal 0 reattaches to data-database-0 after Pod recreation.
Scale-up adds higher ordinals in order; scale-down removes the highest ordinals first while default PVC retention keeps claims for removed ordinals. Image updates roll through replicas in reverse ordinal order with RollingUpdate. Use a Deployment when replicas are interchangeable. Reach for a StatefulSet when clients or cluster software must address distinct ordinals with optional per-replica disks. When Pods stay Pending, inspect StorageClass availability and PVC binding before chasing DNS or rollout issues.

