DaemonSets

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

StatefulSets covers the controller that gives each Pod a stable identity tied to an ordinal; a DaemonSet is the other axis a controller can bind Pods to: instead of a replica count, it binds a Pod to every node (or every node matching a selector), one copy per node, and keeps that mapping true as nodes join or leave the cluster. This page is written against Kubernetes 1.37; see the section index for the full version baseline. Unlike most of this section, no O’Reilly book chapter covers DaemonSets — both books' sampler material omits the chapter (Book 2’s own table of contents lists it as one of the chapters the 1st-edition sampler leaves out), so everything below is drawn straight from the official documentation, with no "book era" table to reconcile.

What a DaemonSet is, and when to reach for one

A Deployment (ReplicaSets and Deployments) answers "keep N replicas of this Pod running somewhere in the cluster", and the scheduler is free to place all N of them on the same node or spread them however it likes. A DaemonSet answers a different question entirely: "run exactly one copy of this Pod on every node that matches this selector, and keep it that way as nodes are added or removed" — there is no replicas field at all, because the desired count is derived from the number of matching nodes, not set by hand. Three use cases come up repeatedly, and all three share the same shape: a piece of infrastructure that has to exist per node, not per application:

  • A log shipper — an agent such as Fluent Bit or Filebeat that tails every container’s log files on a node and forwards them to a central store, so logs survive Pod deletion instead of only living in kubectl logs.

  • A node exporter — a metrics agent such as Prometheus’s node_exporter that reads /proc and /sys on the host to expose CPU, memory, disk and network metrics for that node, one target per node (Prometheus: Containers and Kubernetes Monitoring covers scraping these).

  • A CNI or networking agent — Calico’s calico-node, Cilium’s agent, or kube-proxy itself (where it still runs as a DaemonSet rather than as a static Pod), each of which must be present on every node before that node can route Pod traffic at all, including on control-plane nodes that would otherwise run no application workload.

All three need kubectl get pods -n <namespace> -o wide to show exactly one Pod per node with a NODE column that never repeats, which is the DaemonSet controller’s whole job: it watches the cluster’s node list the same way a ReplicaSet watches its Pods, and reconciles "one Pod here" for every node the selector matches, creating a Pod on a newly-joined node and deleting the Pod on a node that is removed or stops matching.

Scheduling: nodeSelector, node affinity and tolerations

By default, with no selector at all, a DaemonSet targets every node in the cluster — including control-plane nodes, which is exactly what a CNI agent or kube-proxy needs and what an application-level log shipper usually does too, since logs exist on every node regardless of what runs there. Narrowing that set uses the same two mechanisms any other Pod does, covered in full in Scheduling and Disruptions:

  • spec.template.spec.nodeSelector restricts the DaemonSet to nodes carrying a given label, e.g. only nodes labeled node-role.kubernetes.io/edge for an edge-specific agent.

  • spec.template.spec.affinity.nodeAffinity expresses the same restriction with the fuller requiredDuringSchedulingIgnoredDuringExecution / preferredDuringSchedulingIgnoredDuringExecution syntax, needed once the condition is more than a flat label match (an In/NotIn/Exists expression, or several terms ORed together). Internally, the DaemonSet controller itself compiles a per-node nodeAffinity term onto each Pod it creates — since Kubernetes 1.17 it no longer sets spec.nodeName directly, it lets the default scheduler place the Pod exactly as it would any other, which is what lets a DaemonSet Pod also carry ordinary Pod- and node-affinity rules, resource requests, and priority classes rather than bypassing the scheduler entirely.

Reaching every node, including ones with a NoSchedule or NoExecute taint (most commonly node-role.kubernetes.io/control-plane, or a custom taint on a dedicated node pool), needs a matching tolerations entry on the Pod template — without it, the DaemonSet’s own Pod is scheduled everywhere except the tainted nodes, silently leaving them without the agent. A handful of tolerations for conditions the cluster itself manages (node.kubernetes.io/not-ready, node.kubernetes.io/unreachable, node.kubernetes.io/disk-pressure, node.kubernetes.io/memory-pressure, node.kubernetes.io/unschedulable, all with effect: NoExecute or NoSchedule) are added automatically by an admission controller for every DaemonSet Pod, precisely so a node agent — especially a CNI agent, which is often what would clear those conditions — keeps running through the conditions it exists to fix. A control-plane taint is never added automatically; it must be tolerated explicitly, as in the log-agent example below.

A Fluent Bit-style log-agent DaemonSet

The manifest below follows the shape of the official Fluent Bit Kubernetes logging guide: a DaemonSet mounts the node’s own log directory read-only through a hostPath volume (Volumes and Ephemeral Storage covers hostPath and its risks in full), reads its own configuration from a ConfigMap (ConfigMaps), and tolerates the control-plane taint so every node — including the ones running only cluster infrastructure — gets its logs shipped. The configuration below is a placeholder: it tails Pod log files and writes parsed records to standard output, not to any real backend, so the example has no credentials or endpoint of its own to leak.

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: bookshelf
data:
  fluent-bit.conf: |
    [SERVICE]
        Flush        5
        Log_Level    info

    [INPUT]
        Name              tail
        Path              /var/log/pods/bookshelf_*/*/*.log
        multiline.parser  cri    # kind runs containerd, which writes CRI-format logs
        Tag               bookshelf.*

    [OUTPUT]
        Name   stdout
        Match  *
        # Replace with a real backend, e.g. Elasticsearch or a hosted logging service.
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit-agent
  namespace: bookshelf
  labels:
    app.kubernetes.io/name: fluent-bit-agent
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: fluent-bit-agent
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: fluent-bit-agent
    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
      containers:
        - name: fluent-bit
          image: fluent/fluent-bit:3.1
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 128Mi
          volumeMounts:
            - name: varlogpods
              mountPath: /var/log/pods
              readOnly: true
            - name: config
              mountPath: /fluent-bit/etc/
      volumes:
        - name: varlogpods
          hostPath:
            path: /var/log/pods
        - name: config
          configMap:
            name: fluent-bit-config

A node exporter or CNI agent DaemonSet looks the same at the controller level — one Pod per selected node, the same tolerations/updateStrategy shape — but typically also sets hostNetwork: true and hostPID: true so the container observes the node’s own network interfaces and process table directly, rather than the Pod’s own isolated namespaces; that trade-off (and the security review it deserves) belongs to Pod Security, not repeated here.

kubectl apply -f fluent-bit-daemonset.yaml
kubectl get daemonset fluent-bit-agent -n bookshelf
kubectl get pods -n bookshelf -l app.kubernetes.io/name=fluent-bit-agent -o wide
NAME                DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
fluent-bit-agent     3         3         3       3            3           <none>          2m

NAME                     READY   STATUS    RESTARTS   AGE   NODE
fluent-bit-agent-4f2xk   1/1     Running   0          2m    kind-worker
fluent-bit-agent-9m7qz   1/1     Running   0          2m    kind-worker2
fluent-bit-agent-t8v2n   1/1     Running   0          2m    kind-control-plane
flowchart TB DS["DaemonSet: fluent-bit-agent\n(selector matches all nodes,\ntolerates control-plane taint)"] subgraph N1["Node: kind-control-plane (tainted)"] P1[Pod fluent-bit-agent] end subgraph N2["Node: kind-worker"] P2[Pod fluent-bit-agent] end subgraph N3["Node: kind-worker2"] P3[Pod fluent-bit-agent] end DS -->|one Pod per node| P1 DS -->|one Pod per node| P2 DS -->|one Pod per node| P3

Update strategy: RollingUpdate and OnDelete

spec.updateStrategy.type controls how an edited Pod template reaches the Pods a DaemonSet already runs — the same question ReplicaSets and Deployments answers for a Deployment’s spec.strategy, with a smaller set of knobs because there is exactly one Pod per node rather than an arbitrary replica count to shift between two ReplicaSets:

updateStrategy.type Behaviour

RollingUpdate (default)

The controller deletes and recreates a bounded number of node Pods at a time, oldest first, until every node runs the new template. rollingUpdate.maxUnavailable (default 1) bounds how many of the DaemonSet’s Pods may be unavailable at once, exactly like a Deployment’s field of the same name. rollingUpdate.maxSurge (GA since Kubernetes 1.25) lets the controller instead create the replacement Pod alongside the old one on the same node before deleting the old one, trading a brief moment of two Pods on one node for zero coverage gap. That zero-gap rollout is maxSurge: 1 with maxUnavailable: 0; the two fields cannot both be 0, exactly as with a Deployment.

OnDelete

The controller only ever updates its own template; it never touches an existing Pod. A new node still gets a Pod running the new template immediately, but every already-running Pod keeps running the old template until something else deletes it — a person, a node drain, or a script — at which point its replacement picks up the new template. This is the right choice for an agent so sensitive (or so tied to node-level state) that its rollout must be driven one node at a time, by hand or by a controlled script, rather than trusted to the controller’s own pacing.

kubectl set image daemonset/fluent-bit-agent fluent-bit=fluent/fluent-bit:3.2 -n bookshelf
kubectl rollout status daemonset/fluent-bit-agent -n bookshelf
kubectl rollout history daemonset/fluent-bit-agent -n bookshelf
kubectl rollout undo daemonset/fluent-bit-agent -n bookshelf

kubectl rollout status, history and undo work against a DaemonSet exactly as they do against a Deployment (ReplicaSets and Deployments's kubectl rollout section applies unchanged); kubectl scale does not, because a DaemonSet has no replicas field to scale.

DaemonSets vs static Pods

Both a DaemonSet Pod and a static Pod end up running on every targeted node without an explicit nodeName per-Pod assignment, which invites confusing the two, but they solve different problems at different layers:

DaemonSet Static Pod

Who creates the Pod

The DaemonSet controller, running in kube-controller-manager, via the normal API server and scheduler.

The kubelet on that node, reading manifest files from a local directory (or, less commonly, a URL) that the kubelet itself polls — no controller, no scheduler, no API server round trip required to create it.

Requires a working control plane

Yes — the API server, controller manager and scheduler must already be up.

No — this is precisely why kubeadm bootstraps kube-apiserver, etcd, kube-scheduler and kube-controller-manager themselves as static Pods: there is no control plane yet to schedule them onto nodes.

Visible via kubectl

Fully — kubectl get daemonset, kubectl describe, kubectl delete, kubectl edit all work normally.

A read-only mirror Pod is published to the API server so kubectl get pods and kubectl logs work, but kubectl delete pod on it does nothing lasting — the kubelet notices the mirror Pod is gone and republishes it from its local manifest; the manifest file itself is what must be edited or removed.

Update mechanism

updateStrategy above, driven centrally for the whole fleet.

Editing (or removing) the manifest file on that one node — there is no fleet-wide update mechanism at all; each node’s kubelet only ever looks at its own manifest directory.

In practice static Pods are reserved for the small set of components that must exist before there is a cluster to schedule anything onto — the control-plane components kubeadm installs this way, and occasionally a node-local break-glass debugging Pod — while every node-level application agent (a log shipper, a metrics exporter, most CNI plugins) is a DaemonSet, because it needs the API-server visibility, centralized rollout and RBAC that a static Pod’s manifest file cannot provide.