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-2 and 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.local resolves to that specific pod. This is how replicas find their primary without a service discovery sidecar.
  • Each pod keeps its own PVC through volumeClaimTemplates, so postgres-1 always 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.
ControllerIdentityRight for
DeploymentRandom, interchangeableStateless web and API services
StatefulSetOrdinal, stable, orderedDatabases, queues, consensus systems
DaemonSetOne per nodeNode-level agents and drivers
JobRuns to completionMigrations, batch work, one-off tasks
CronJobJob on a scheduleNightly 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-ready is 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 activeDeadlineSeconds to 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.
  • parallelism above 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 startingDeadlineSeconds decides whether a late run still fires.
⚠️
Cron schedules are evaluated in UTC unless you set 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?
You can, and the data will be fine as long as there is exactly one replica and a single-writer volume. The moment you scale it or the pod is rescheduled during a rolling update, identity and ordering are what you will miss. Use a StatefulSet.
Why did my CronJob stop producing Jobs?
Check kubectl describe cronjob. A previous run still active under Forbid, a missed startingDeadlineSeconds, or a suspend flag are the usual answers.

Storage: volumes, PersistentVolumes and StorageClasses Scheduling: affinity, taints and tolerations

Last refreshed 2026-09-18.