Skip to main content
Docs navigation

Run in a container

The runner is a plain foreground process — it runs fine in a container. What changes isn't whether it runs; it's what it can see.

docker host
$ docker run -d --name emisar \
    -v emisar-data:/var/lib/emisar \
    -e EMISAR_URL=https://emisar.dev \
    -e EMISAR_ENROLLMENT_KEY=emkey-enroll-… \
    you/emisar-runner
$ docker logs -f emisar
Egress-only
Nothing listens inbound — the container dials out to emisar.dev like any other runner.

What a containerized runner sees#

A runner can only act on what its own namespace can reach. On a VM or bare metal that's the whole machine — which is why the host install is the default: most packs are host-oriented, and some literally require a systemd host. Put the same runner in a bare container and those packs now see the container — its processes, its filesystem, its network.

It gives you the freedom to design the runner scope that fits your specific use case. A sidecar that can only see one app is a tightly scoped runner — the agent operating through it can't touch anything else. You just have to pick the shape that matches what you want the agent to operate: one app, a node, or every node.

The two mechanics every shape shares#

First, skip the service. The default installer creates a systemd service, so on a host without a real init it refuses with "this looks like a container or cloud shell" — that check is about the installer, not the binary. Pass --no-service and it skips init detection, the service user, and the unit, and just puts the binary in place. Supervision then belongs to whatever runs the container: docker run --restart, a Kubernetes restart policy, or the Nomad client.

shell
$ curl -sSL https://emisar.dev/install.sh | sudo bash -s -- --no-service

Second, preserve connection state. By default, the runner uses the container hostname as its identity. After the first connect, the per-runner token lands at cloud.token_path (/var/lib/emisar/token.json by default). Keep the hostname stable and /var/lib/emisar in a volume so restarts preserve the same runner and its durable dispatch state. Pin runner.id when the orchestrator changes hostnames across placements but the workload is still one logical runner:

config.yaml
schema_version: 1
runner:
  id: "payments-sidecar"
  group: "sidecars"
paths:
  data_dir: "/var/lib/emisar"
events:
  jsonl_path: "/var/log/emisar/events.jsonl"
cloud:
  enrollment_key_env: "EMISAR_ENROLLMENT_KEY"
  token_path: "/var/lib/emisar/token.json"

One more thing before a fleet: the dashboard's Connect-a-runner key is single-use — right for one host, wrong for a template that scales. For DaemonSets and system jobs, mint a reusable enrollment key (optionally capped at a number of uses) and hand that to the template — see enrollment keys.

A sidecar next to one app#

When the thing you want the agent to operate is the app — read its logs, inspect its processes, poke its admin CLI — run the runner as a second container that joins the app container's PID, network, and (read-only) volumes. The runner then sees exactly that app and nothing else; policy, approvals, and audit work unchanged.

shell
$ docker run -d --restart unless-stopped \
    --name emisar-runner \
    --pid container:payments \
    --network container:payments \
    --volumes-from payments:ro \
    -e EMISAR_URL=https://emisar.dev \
    -e EMISAR_ENROLLMENT_KEY=emkey-enroll-… \
    -v /etc/emisar/config.yaml:/etc/emisar/config.yaml:ro \
    -v emisar-runner-data:/var/lib/emisar \
    your-registry/emisar-runner

emisar does not publish a universal runner image. Build yours from the release binary with exactly the tools its selected packs call; the command above supplies its operator-owned config and persistent token volume. The image contents are part of the runner's capability boundary, so keep them as narrow as the workload allows.

One per node: Kubernetes and Nomad#

To operate the nodes instead of a single app, run one runner per node — a Kubernetes DaemonSet or a Nomad system job. Both reuse the two mechanics above (a service-less binary and a persistent state volume) and a reusable enrollment key; each has its own worked manifest:

  • Kubernetes — a DaemonSet whose pod spec is your blast-radius dial: hostPID/hostNetwork and read-only host mounts decide exactly what each node's runner can see.
  • Nomad — a system job placing one instance on every client node, with the release binary fetched in an artifact stanza and identity persisted on a host volume.