Docs navigation
Get started
AI agents
Connect
Operate
Day to day
When it breaks
Govern access
Team & account
Access
Identity concepts
Provider guides
Account
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.
- A host with a container runtime. The examples use Docker.
-
Outbound HTTPS from the container to
emisar.dev:443for the control plane, plusregistry.emisar.dev:443for 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 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:
$ 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:
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:
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.
$ 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.
$ 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.