StatefulSets, DaemonSets and Jobs
Workloads that are not just replicas: stable identity for stateful services, one pod per node, and work that must run to completion.
StatefulSets give pods an identity
A Deployment generates pods with random suffixes, starts them in any order and treats them as interchangeable. A StatefulSet does the opposite: predictable names, ordered creation and deletion, and a network identity that survives a restart.
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
clusterIP: None # headless: DNS returns pod IPs, no virtual IP
selector:
app: postgres
ports:
- port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
podManagementPolicy: OrderedReady # OrderedReady | Parallel
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine- Pods are named
postgres-0,postgres-1,postgres-2and are created one at a time, each waiting to become Ready before the next starts. - Scaling down or rolling back goes in reverse order, highest ordinal first — which matters because a replica is usually the one that may be safely removed.
postgres-0.postgres.namespace.svc.cluster.localresolves to that specific pod. This is how replicas find their primary without a service discovery sidecar.- Each pod keeps its own PVC through
volumeClaimTemplates, sopostgres-1always gets the same disk back after a reschedule. - A StatefulSet cannot do anything about quorum. Application-level peer discovery and primary election are still yours to configure.
| Controller | Identity | Right for |
|---|---|---|
| Deployment | Random, interchangeable | Stateless web and API services |
| StatefulSet | Ordinal, stable, ordered | Databases, queues, consensus systems |
| DaemonSet | One per node | Node-level agents and drivers |
| Job | Runs to completion | Migrations, batch work, one-off tasks |
| CronJob | Job on a schedule | Nightly reports, cleanup, polling |
DaemonSets run one pod on every node
A DaemonSet places exactly one pod on each node that matches its tolerations, and automatically adds one when a node joins. It is how node agents, log shippers and the CNI itself are deployed.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 10% # 1 would serialise the whole rollout
template:
metadata:
labels:
app: node-exporter
spec:
tolerations: # without these the pod skips tainted nodes
- operator: Exists # tolerate everything, including NotReady
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.2
ports:
- containerPort: 9100
hostPort: 9100
resources:
requests:
cpu: 50m
memory: 64Mi- The kubelet creates DaemonSet pods directly with a node affinity, not through the scheduler, so unschedulable nodes can still qualify if tolerated.
- Tolerating
node.kubernetes.io/not-readyis usually right for a log or metrics agent and usually wrong for anything else. - DaemonSets must be lean. One copy per node multiplies the request across the whole cluster.
Jobs and CronJobs
A Job runs pods until a target number of successful completions is reached, then stops. A CronJob creates Jobs on a schedule.
apiVersion: batch/v1
kind: Job
metadata:
name: migrate
spec:
completions: 1
parallelism: 1
backoffLimit: 2 # retries before the Job is marked Failed
ttlSecondsAfterFinished: 3600 # let the controller clean up an hour later
template:
spec:
restartPolicy: Never # required: Never or OnFailure, never Always
containers:
- name: migrate
image: ghcr.io/example/app:1.4.0
command: ["python", "-m", "app.migrate"]
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 2 * * *" # in UTC
timeZone: "Europe/London"
concurrencyPolicy: Forbid # Allow | Forbid | Replace
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: report
image: ghcr.io/example/app:1.4.0
command: ["python", "-m", "app.report"]kubectl get jobs -w
kubectl logs job/migrate
kubectl describe job migrate # events show why a pod was retried
kubectl get cronjobs
kubectl create job --from=cronjob/nightly-report manual-run-1- Job pods are ordinary pods, so they need requests, limits and a sensible
activeDeadlineSecondsto stop a hung run. - Retries only help if the work is idempotent. A migration that half-applied and then re-runs can do more damage than the original failure.
parallelismabove one requires your job to split work without coordinating, for example by claiming rows or sharding a directory.- A scheduler miss is possible if the control plane is busy, so
startingDeadlineSecondsdecides whether a late run still fires.
timeZone, and a slow run will overlap the next one under the default concurrencyPolicy: Allow. For anything that writes to a shared resource, set Forbid and an activeDeadlineSeconds.FAQ
Can I put a database in a Deployment?
Why did my CronJob stop producing Jobs?
kubectl describe cronjob. A previous run still active under Forbid, a missed startingDeadlineSeconds, or a suspend flag are the usual answers.Related
Storage: volumes, PersistentVolumes and StorageClasses Scheduling: affinity, taints and tolerations
Last refreshed 2026-09-18.