Skip to main content
Docs navigation

Kubernetes DaemonSet

Run one runner per node with a DaemonSet. The pod spec controls what each runner can read or change. This example uses the official image and persistent host storage.

To read what a containerized runner can reach, see Install in a container.

Before you start, you need:
  • kubectl access that can create namespaces, Secrets, and DaemonSets.
  • Outbound HTTPS from every node to emisar.dev:443 and registry.emisar.dev:443. See Network requirements for the optional GitHub fallback domains.

Prepare the deployment#

This manifest grants node-wide access

This manifest runs as root and reads node processes, networks, journals, and files. Remove every grant that your packs do not need.

  1. Create a namespace named emisar-system. Configure its admission policy to allow this host-access profile.
  2. Create a reusable enrollment key.
  3. Create the Secret. Enter the key at the hidden prompt.
shell
kubectl create namespace emisar-system
read -rsp "Enrollment key: " EMISAR_ENROLLMENT_KEY
printf '\n'
kubectl -n emisar-system create secret generic emisar-runner \
  --from-literal=enrollment-key="$EMISAR_ENROLLMENT_KEY"
unset EMISAR_ENROLLMENT_KEY

Pick the image: each runner release notes its image digest. Replace <image-digest> with the 64 hex characters after sha256:.

The DaemonSet#

This spec lets the runner observe each node. The node name is the runner identity, and the runner token, dispatch state, and local journal stay on the node.

yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: emisar-runner
  namespace: emisar-system
spec:
  selector:
    matchLabels:
      app: emisar-runner
  template:
    metadata:
      labels:
        app: emisar-runner
    spec:
      hostPID: true      # see every process on the node
      hostNetwork: true  # see the node's real interfaces and ports
      containers:
        - name: runner
          image: ghcr.io/andrewdryga/emisar-runner@sha256:<image-digest>
          securityContext:
            runAsUser: 0  # journald + /host reads are root-only; see below
          env:
            - name: EMISAR_GROUP
              value: "k8s-nodes"  # actions can target the whole group — one per fleet
            - name: EMISAR_RUNNER_ID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName  # the node, not the pod, is the identity
            - name: EMISAR_ENROLLMENT_KEY
              valueFrom:
                secretKeyRef:
                  name: emisar-runner
                  key: enrollment-key
          volumeMounts:
            - name: state
              mountPath: /var/lib/emisar
            - name: audit-journal
              mountPath: /var/log/emisar
            - name: host-root      # node filesystem, read-only, for file inspection
              mountPath: /host
              readOnly: true
            - name: journal        # journald logs on systemd nodes
              mountPath: /var/log/journal
              readOnly: true
            - name: systemd-run    # systemd state, for unit status reads
              mountPath: /run/systemd
              readOnly: true
      volumes:
        - name: state
          hostPath:
            path: /var/lib/emisar
            type: DirectoryOrCreate
        - name: audit-journal
          hostPath:
            path: /var/log/emisar
            type: DirectoryOrCreate
        - name: host-root
          hostPath:
            path: /
        - name: journal
          hostPath:
            path: /var/log/journal
        - name: systemd-run
          hostPath:
            path: /run/systemd

Deploy and verify#

  1. Save the manifest as emisar-runner.yaml.
  2. Run kubectl apply -f emisar-runner.yaml.
  3. Run kubectl -n emisar-system rollout status daemonset/emisar-runner --timeout=5m.
  4. Open Runners. Confirm each node has one online runner. Tainted control-plane nodes are skipped unless you add a toleration.
  5. Run debugging.loadavg on one node runner.
  6. Open Audit. Confirm that the event records the runner, action, operator, and reason.

What the pod spec grants#

hostPID exposes node processes. hostNetwork exposes node interfaces and ports. The read-only mounts expose the journal, systemd state, and node files under /host.

The manifest uses runAsUser: 0 because most node files and journals require root; the image otherwise runs as user 65532. Actions run the programs inside the image, and the official image carries only its documented observation tools — if a pack needs another program, build an extended image.

The host mounts are read-only except for runner state and its local journal. Remove runAsUser, hostPID, hostNetwork, or any mount your packs do not need.

Letting a node action mutate#

A node mutation needs more than the read-only profile. Restarting a unit, for example, needs writable systemd and D-Bus sockets — mount /run/systemd and /run/dbus writable. Grant exactly what the intended actions need and nothing more; the runner's user and polkit pattern on the Linux host page is the same discipline on a host.

After it is connected#

Manage the runner fleet covers work after the first connection — groups, labels, pack credentials, updates, reconnects, and removal. To roll the DaemonSet across clusters in phases, see the Go to production.

Last reviewed August 18, 2026