StatefulSets and DaemonSets
The idea in one sentence
Section titled “The idea in one sentence”A StatefulSet gives each of its Pods a stable, unique identity and its own persistent storage across rescheduling, while a DaemonSet guarantees exactly one matching Pod runs on every node — two controllers that solve “identity” and “coverage” instead of the plain replica count a Deployment gives you.
StatefulSets: stable identity and per-Pod storage
Section titled “StatefulSets: stable identity and per-Pod storage”Where a Deployment’s Pods are interchangeable, a StatefulSet’s Pods get predictable ordinal names — db-0, db-1, db-2 — and each ordinal keeps its own PersistentVolumeClaim, created from volumeClaimTemplates, across restarts and rescheduling. db-1 always comes back with the same volume it had before, on whatever node it lands on. A StatefulSet also requires a headless Service (clusterIP: None) so that each Pod gets its own stable DNS name, instead of the Service load-balancing across all of them.
apiVersion: v1kind: Servicemetadata: name: db labels: app: dbspec: clusterIP: None selector: app: db ports: - port: 5432 name: postgres---apiVersion: apps/v1kind: StatefulSetmetadata: name: dbspec: serviceName: db replicas: 3 selector: matchLabels: app: db template: metadata: labels: app: db spec: containers: - name: postgres image: myregistry/postgres:16 ports: - containerPort: 5432 name: postgres volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumeClaimTemplates: - metadata: name: data spec: accessModes: ['ReadWriteOnce'] resources: requests: storage: 10GiThis is the shape you reach for whenever members need a stable peer identity: databases, and any distributed system where node 0 is special or peers must find each other by a fixed name rather than by whichever Pods happen to exist right now.
# Pods come up in order: db-0, then db-1, then db-2kubectl get pods -l app=db -o wide
# Each Pod has its own stable DNS name via the headless Servicekubectl run -it --rm debug --image=busybox:1.36 --restart=Never -- \ nslookup db-0.db.default.svc.cluster.local
# Scaling and deletion are ordered too: highest ordinal firstkubectl scale statefulset/db --replicas=2Deployment, scaling, and deletion of a StatefulSet’s Pods all happen one at a time, in order — db-1 will not start until db-0 is running and ready, and scaling down removes the highest ordinal first. That ordering is what makes it safe to depend on “the previous member is already up” in a distributed system’s bootstrap logic.
DaemonSets: one Pod on every node
Section titled “DaemonSets: one Pod on every node”A DaemonSet does not take a replicas count at all — instead it schedules exactly one matching Pod onto every node in the cluster (or a subset, if you scope it with a node selector or affinity rule). As nodes join the cluster, the DaemonSet controller automatically schedules its Pod onto them; as nodes leave, their Pod goes with them. This is the shape for node-level agents: log collectors, monitoring and metrics agents, and network plugins.
apiVersion: apps/v1kind: DaemonSetmetadata: name: node-log-agentspec: selector: matchLabels: app: node-log-agent template: metadata: labels: app: node-log-agent spec: containers: - name: log-agent image: myregistry/log-agent:3.0.0 volumeMounts: - name: varlog mountPath: /var/log volumes: - name: varlog hostPath: path: /var/log# One Pod per matching node -- the count follows the cluster, not a replicas fieldkubectl get pods -l app=node-log-agent -o wideflowchart TB
subgraph ss["StatefulSet db (ordered identity)"]
w0["db-0"] --- s0[("data-db-0")]
w1["db-1"] --- s1[("data-db-1")]
w2["db-2"] --- s2[("data-db-2")]
end
subgraph ds["DaemonSet node-log-agent (one per node)"]
n1["Node 1"] --> a1["log-agent"]
n2["Node 2"] --> a2["log-agent"]
n3["Node 3"] --> a3["log-agent"]
end Three controllers, three different concerns
Section titled “Three controllers, three different concerns”A Deployment does not care which node a replica lands on, and its Pods are interchangeable by design. A StatefulSet cares about stable identity per ordinal — same name, same storage, every time. A DaemonSet cares about node coverage — one Pod per node, a count you never set yourself because it is derived from the cluster’s node count.