opschain runner
Included from Basic tier
Runs an OpsChain runner in the foreground against a server: on the first run
it registers with the registration token in OPSCHAIN_RUNNER_TOKEN (or
--token), then serves the
long-poll work loop — acquiring queued steps, executing them with its
advertised executors, and streaming results back. The server address resolves
through the global target precedence — --server <url>,
OPSCHAIN_SERVER, or the active context — the same way every other
server-facing command finds its server.
On start it prints the executor set it advertises:
advertising executors: hostexec,containerexec.
Every line the runner prints — the start-up line, registered runner …,
the per-job and failover notices, the stop narrative — goes to one
sink: the rotated file named by the global --log-file flag when it is
set, the process's stderr otherwise. Under --log-file nothing is written
to the console; tail the file for the whole story.
Flags
| Flag | Meaning |
|---|---|
--server <url>[,<url>…] | Server base URL (global flag; default: OPSCHAIN_SERVER or the active context). A trailing / is stripped. For the runner it accepts an ordered comma-separated list — the primary first, failover targets after; see "Multiple servers" below. |
--log-file <path> | Global flag: write the runner's output to this file with built-in size+age+count rotation (default rotation: 100 MB, 30 days, 10 backups) instead of stderr. Every line the runner prints — including the draining: / drained: / goodbye: stop narrative — lands in the file; nothing is left on the console. |
--token <token> | Registration token — required on the first run; afterwards the saved identity in --dir is used. Default: $OPSCHAIN_RUNNER_TOKEN. Prefer the environment variable: a flag value is visible to every process on the host, and a token enrols runners for as long as it lives — mint it single-use (opschain docs cli/admin-runner-token-create) when one host is all it should enrol. |
--name <name> | Runner display name (default: hostname). |
--executors auto|<list> | Executor kinds to advertise: auto (hostexec, plus containerexec when a container runtime is detected) or an explicit comma-separated list (hostexec,containerexec — naming containerexec fails when no runtime is reachable). |
--labels <list> | Comma-separated runner labels, matched by step placement. |
--dir <path> | Runner state directory (default: <opschain home>/runner). |
--env-passthrough <list> | Comma-separated extra environment variable names handed from the runner's own env to every step (default: $OPSCHAIN_RUNNER_ENV_PASSTHROUGH). OPSCHAIN_VAULT_* names are dropped from it: the runner's vault access is the secrets_providers section of its config file, never its process environment. |
--spool-max-mb <n> | Per-step disk log spool budget in MB; over budget the oldest chunks are dropped under a gap marker (default 64). |
--max-parallel-steps <n> | Declared step capacity: the most steps of one pinned run this runner executes at once, clamping the run's runner.max_parallel_steps setting to min(offered, declared). Size it as N × per-step peak RSS ≤ the host/container memory. Default: $OPSCHAIN_RUNNER_MAX_PARALLEL_STEPS, else unlimited — follow the server's cap. When the clamp binds, the run's step log carries one notice naming both numbers. |
The config file: <dir>/config.yaml
The runner reads config.yaml under its state directory at start. Its
secrets_providers: section declares, per secrets provider code the
estate registered with resolution: runner, how this runner reaches
that vault — kind, address, mount, the auth method and its material
(credential, credential_env or credential_file), TLS posture — and
the codes become the capability the runner declares for placement
(opschain docs runner-executors has the full shape). On start the
runner prints resolving secrets providers at the runner: <codes>; a
misconfigured entry refuses the start naming the code.
A host that still carries the retired OPSCHAIN_VAULT_* environment
and has no secrets_providers section refuses to start:
`OPSCHAIN_VAULT_* is set in the runner's environment but
Subcommands
| Command | Meaning |
|---|---|
opschain runner migrate-vault-env | One-off: write the secrets_providers section from the retired OPSCHAIN_VAULT_* environment and exit (opschain docs cli/runner-migrate-vault-env). |
Environment
| Variable | Meaning |
|---|---|
OPSCHAIN_RUNNER_ENV_PASSTHROUGH | Default for --env-passthrough. |
OPSCHAIN_RUNNER_MAX_PARALLEL_STEPS | Default for --max-parallel-steps (a positive integer; anything else reads as unset). |
OPSCHAIN_RUNNER_RENEW_INTERVAL | Lease-renewal interval override (a Go duration, e.g. 30s). |
OPSCHAIN_RUNNER_POLL_TIMEOUT | Long-poll timeout override (a Go duration). |
Multiple servers (ordered failover list)
--server https://site-a.example,https://site-b.example (or the same in
OPSCHAIN_SERVER) registers the runner with an ordered server list: the
first entry is the primary, later entries are failover targets. The list is
recorded in the saved identity (runner.json gains server_urls; an
identity saved before the field keeps working as a one-server list).
- Failover happens on sustained unreachability — three consecutive
transport failures to the current server (a refused or dropped
connection, or a load balancer's own error page because no backend is
left) — and the runner prints
server unreachable after 3 consecutive attempts: failing over to <url>. A server that answersunavailable(a replica whose log-ingest owner has not been adopted yet after a peer died, the system-quiesce write barrier, a replica whose database is unreachable —store unavailable: …) has answered: the call is retried on the same server and never counts toward failover. Through such an answer the in-flight job keeps running and renewing, its log chunks stay spooled until they land, and its result is re-submitted for up to ten minutes before the five-attempt budget applies — a database outage shorter than the lease TTL costs nothing. A chunk the server refuses by verdict instead leaves the spool withlog chunk N dropped: the server refused it (…); the gap is reported to the serverin the runner's log: a gap record (the sequence and a reason class, never the content) takes the chunk's place in the spool and is uploaded and retried like a chunk, so the step's log shows*** log gap: 1 chunk(s) N..N not delivered — <reason> ***where the line would have been and the run recordswarn:logs:chunks_dropped. The same happens for a spool entry that cannot be read back (spool_unreadable); chunks still spooled when the end-of-step flush times out are reported on the result submission (seal_timeout), and a chunk the server answersfailed_preconditionfor arrived after the attempt's log sealed — the server marked that gap itself. An in-flight job survives the switch: its lease renewals, log chunks and result submission continue against the next server (every replica honours the job's token through the shared database). - Failback is handshake-governed: every session handshake tries the
primary first, so the runner returns to it on its next natural
re-handshake after the primary recovers (
server failover: now using <url>). - Changing the list re-registers, exactly like changing the server: a
saved identity whose list differs from the configured one refuses with
runner is registered with <list>; remove <dir> to re-register. - A server that ANSWERS — even with a refusal — is never failed over: it speaks for the shared estate, so its verdict stands.
Signals
SIGTERM or SIGINT drains gracefully: the runner finishes the in-flight job
and stops acquiring, printing draining: finishing the in-flight job, no new work (signal again to stop hard) once, on the first signal. A second
signal stops hard.
Once the in-flight job has finished (at once when idle) the runner prints
drained: no longer acquiring jobs, announces its exit to the server and
prints goodbye: the server marked this runner stopped — the fleet view
reads it stopped immediately and an info:runner:goodbye event is
recorded. A goodbye the server did not answer within 2 s prints goodbye not delivered (the server will age this runner out) instead.
The three lines are one narrative and land in one place, in that order:
the --log-file when one is set (the goodbye line is written before the
runner exits, so the file always ends on it), stderr otherwise. Nothing of
the stop is printed to the console under --log-file — a tail of the log
file is the complete record of a graceful stop.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Clean shutdown. |
| 1 | Registration or the work loop failed (an unreachable container runtime named in --executors, a misconfigured secrets_providers entry, or the retired OPSCHAIN_VAULT_* environment with no section — included). |
| 2 | Usage error (the runner needs a server target — pass --server <url>, set OPSCHAIN_SERVER, or 'opschain context use <name>'). |
Examples
First run — the token rides the environment, never the argv:
OPSCHAIN_RUNNER_TOKEN=<registration-token> opschain runner --server https://opschain.example.com --labels linux,build
Host-only execution with a bigger log spool:
opschain runner --server https://opschain.example.com --executors hostexec --spool-max-mb 256
See also
opschain docs fleet-queue— how queued steps reach runners and why one might not dispatch.opschain learn 6.1— runners and the fleet.