opschain runner
Included from Basic tier
Runs an OpsChain runner in the foreground against a server: on the first run
it registers with the --token registration 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.
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. |
--token <token> | Registration token — required on the first run; afterwards the saved identity in --dir is used. |
--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). A passed-through OPSCHAIN_VAULT_TOKEN reaches the step as a step-scoped child token minted at dispatch, never as the runner's own value. |
--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. |
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). 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. With
--log-file, these lines land in the log file (the runner's operational
output) while the draining: acknowledgement and the start-up
advertising executors: line stay on stderr — read the log file for the
stop narrative.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Clean shutdown. |
| 1 | Registration or the work loop failed (an unreachable container runtime named in --executors included). |
| 2 | Usage error (the runner needs a server target — pass --server <url>, set OPSCHAIN_SERVER, or 'opschain context use <name>'). |
Examples
First run:
opschain runner --server https://opschain.example.com --token <registration-token> --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.