Skip to main content
Docs navigation

Container

The runner is a plain foreground process, so it runs in a container. The container changes what the runner can see, not whether it runs.

To install straight onto a machine instead, see Linux host.

Before you start, you need:
  • A host with a container runtime. The examples use Docker.
  • Outbound HTTPS from the container to emisar.dev:443 for the control plane, plus registry.emisar.dev:443 for pack installs and updates. Nothing listens inbound, so there is no port to publish. See Network requirements for the optional GitHub fallback domains.

Take the enrollment key from Runners → Connect a runner. Replace X.Y.Z with the current version from the runner release manifest , then start the container:

docker host
$ docker run -d --name emisar \
    -v emisar-data:/var/lib/emisar \
    -v emisar-logs:/var/log/emisar \
    -e EMISAR_ENROLLMENT_KEY=emkey-enroll-… \
    ghcr.io/andrewdryga/emisar-runner:X.Y.Z
$ docker logs -f emisar

The first volume keeps the runner's identity token; the second keeps the hash-chained security journal at /var/log/emisar, so both survive replacing the container.

In the console, open Runners — the container appears online, with the actions its packs advertise.

What a containerized runner sees#

A runner acts only on resources available in its namespace. On a VM or bare metal, that namespace covers the whole machine. Most packs target hosts, and some need systemd — that is why the host install is the default. In a bare container, those packs see only the container's processes, filesystem, and network.

The same limit is the control. A sidecar can join one app's process and network namespaces and mount selected volumes — it then reaches every process, interface, and endpoint in them. You choose whether the agent operates one app, one node, or every node.

The official image#

Every runner release publishes ghcr.io/andrewdryga/emisar-runner for linux/amd64 and linux/arm64. The registry has no latest tag — you always deploy a version you named. What is in the image is part of what the runner can do, so treat an image change with the same care as trusting a new pack.

The image contains the release-built runner binary and three packs that cover what a container can see: fs-search for its filesystem, debugging for its processes, and network-tls for its network — plus only the tools those packs call.

It runs as the non-root user 65532, connects to emisar.dev (set EMISAR_URL for a self-hosted control plane), reads EMISAR_ENROLLMENT_KEY, and keeps state under /var/lib/emisar. The baked config admits only low-risk actions, so the default image can observe but not fix — mount your own config to allow the debugging pack's fix actions.

Each release notes its image digest, and the image carries signed build provenance and an SBOM:

shell
$ gh attestation verify oci://ghcr.io/andrewdryga/emisar-runner:X.Y.Z \
    --repo andrewdryga/emisar \
    --signer-workflow AndrewDryga/emisar/.github/workflows/runner-release-trusted.yml \
    --source-ref refs/tags/runner-vX.Y.Z \
    --deny-self-hosted-runners

Identity and 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 in the official image). Keep the hostname stable and /var/lib/emisar in a volume so restarts preserve the same runner and its durable dispatch state.

The other model is to keep nothing, the way autoscaling fleets treat hosts: no volume, a reusable enrollment key in the template, and a unique hostname per container — Docker's default, the container ID, already is one. Each replacement then registers as a new runner, and the old record sits offline until retention removes it. The unique hostname is required — two containers sharing one hostname register as the SAME runner and fight over its connection — and dispatch state is not preserved: a replaced container abandons whatever it was running.

Two environment variables relabel a fleet without a config file. EMISAR_GROUP sets runner.group. The image uses containers by default. Actions and runbooks can target a whole group at once, so give each fleet its own group instead of pooling unrelated fleets. EMISAR_RUNNER_ID pins runner.id when rescheduling changes the container hostname but it is still the same runner. For labels or admission settings, mount your own file over /etc/emisar/config.yaml:

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"

The key you started with is single-use — right for one container, 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 for the lifecycle details.

Extend it for your packs#

Actions run inside the container, so a pack's client must be in the image too — the postgres pack needs psql there. Extend the official image with the tools your packs call, then install the packs:

Dockerfile
FROM ghcr.io/andrewdryga/emisar-runner:X.Y.Z
USER root
RUN apt-get update \
    && apt-get install -y --no-install-recommends postgresql-client \
    && rm -rf /var/lib/apt/lists/* \
    && emisar pack install postgres=X.Y.Z --hash sha256:…
USER 65532

Pin the pack version and content hash — the pack's page in the pack catalog shows both, with the install command ready to copy — so the image build is reproducible and a tampered registry copy is rejected. Keep the additions as narrow as the workload allows.

Building your own image from scratch instead — a different distro, a hardened base? Install the release binary with --no-service: it skips init detection, the service user, and the systemd unit, and just puts the binary in place. Supervision belongs to whatever runs the container: docker run --restart, a Kubernetes restart policy, or the Nomad client.

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

A sidecar next to one app#

If the agent operates one app, run the runner as a second container beside it. Join the app's PID namespace, network, and read-only volumes; the runner then sees the app's processes and files, and reaches every destination that network can. It cannot see the host's processes or filesystem unless you grant them. Policy, approvals, and audit work unchanged.

shell
$ docker run -d --restart unless-stopped \
    --name emisar-payments \
    --pid container:payments \
    --network container:payments \
    --volumes-from payments:ro \
    -e EMISAR_ENROLLMENT_KEY=emkey-enroll-… \
    -v /etc/emisar/config.yaml:/etc/emisar/config.yaml:ro \
    -v emisar-payments-data:/var/lib/emisar \
    -v emisar-payments-logs:/var/log/emisar \
    ghcr.io/andrewdryga/emisar-runner:X.Y.Z

The official image already covers the core sidecar work: reading the app's files, processes, and network state. The command above adds an operator-owned config and persistent token and journal volumes. Name the container, the volumes, and the config's runner.id after the app — runner.id is also the name the console shows, and the sidecar shares the app container's hostname, so a second sidecar on the same host would otherwise collide on all three. If the agent needs the app's CLI, extend the image with that client.

One per node: Kubernetes and Nomad#

To operate nodes instead of one app, run one runner per node — a Kubernetes DaemonSet or a Nomad system job. Both need an image, a persistent state volume, and a reusable enrollment key. Each guide provides a complete manifest:

  • Kubernetes DaemonSet — the pod spec decides exactly what each node's runner can see: hostPID, hostNetwork, and read-only host mounts.
  • Nomad system job — one runner on every client node; an artifact stanza fetches the release binary, and a host volume keeps the token and dispatch state.

After it is connected#

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

Last reviewed September 3, 2026