Skip to content
docktionary

Kubernetes

StatefulSets, DaemonSets & Jobs

A Deployment assumes every Pod is interchangeable. These three controllers are for when that assumption doesn't hold.

StatefulSet

For workloads that need a stable identity and storage — a clustered database, a message queue. Each replica gets a predictable name (db-0, db-1, ...) and its own PersistentVolumeClaim that follows it across rescheduling, instead of the interchangeable Pods a Deployment manages.

statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db
  replicas: 3
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

Pods are created and scaled in order (db-0 before db-1), which matters for clustered databases that need to join an existing member before they can start.

DaemonSet

Runs exactly one copy of a Pod on every node (or every node matching a selector), automatically extending to new nodes as they join. The standard fit for log shippers, metrics agents, and CNI plugins — anything that needs to be everywhere, not scaled independently.

daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-agent
spec:
  selector:
    matchLabels:
      app: log-agent
  template:
    metadata:
      labels:
        app: log-agent
    spec:
      containers:
        - name: fluent-bit
          image: fluent/fluent-bit:3.1
          volumeMounts:
            - name: varlog
              mountPath: /var/log
      volumes:
        - name: varlog
          hostPath:
            path: /var/log

Job

Runs a Pod to completion — once, or a fixed number of times — and retries it on failure. For one-off or batch work, not long-running services.

job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: docktionary/app:1.4.0
          command: ["npm", "run", "migrate"]

For work that splits across many Pods — processing a batch of files, say — completions sets how many successful Pod runs the Job needs in total, and parallelism caps how many run at once.

yaml
spec:
  completions: 20
  parallelism: 5     # 5 Pods running at a time until 20 have succeeded
  backoffLimit: 3

CronJob

A Job template run on a cron schedule — the cluster-native replacement for a crontab entry, except the thing it runs is a container.

cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: report
              image: docktionary/app:1.4.0
              command: ["npm", "run", "nightly-report"]

concurrencyPolicy decides what happens if a scheduled run is still going when the next one is due: Allow (the default) runs them side by side, Forbid skips the new run entirely, and Replace cancels the still-running one and starts fresh. Forbid is usually the right call for anything that shouldn’t run twice at once, like a report that writes to the same output location.

Checking progress

terminal
kubectl get statefulsets,daemonsets,jobs,cronjobs
kubectl logs job/db-migrate
kubectl get pods -l app=db -w   # watch StatefulSet pods come up in order