Skip to main content
Version: v5.7.0

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​

FlagMeaning
--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

/config.yaml declares no secrets_providers section … run 'opschain runner migrate-vault-env' once`. Run that subcommand (`opschain docs cli/runner-migrate-vault-env`), then start again; with the section present, leftover variables are reported once and never handed to steps.

Subcommands​

CommandMeaning
opschain runner migrate-vault-envOne-off: write the secrets_providers section from the retired OPSCHAIN_VAULT_* environment and exit (opschain docs cli/runner-migrate-vault-env).

Environment​

VariableMeaning
OPSCHAIN_RUNNER_ENV_PASSTHROUGHDefault for --env-passthrough.
OPSCHAIN_RUNNER_MAX_PARALLEL_STEPSDefault for --max-parallel-steps (a positive integer; anything else reads as unset).
OPSCHAIN_RUNNER_RENEW_INTERVALLease-renewal interval override (a Go duration, e.g. 30s).
OPSCHAIN_RUNNER_POLL_TIMEOUTLong-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 answers unavailable (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 with log chunk N dropped: the server refused it (…); the gap is reported to the server in 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 records warn: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 answers failed_precondition for 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​

CodeMeaning
0Clean shutdown.
1Registration 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).
2Usage 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.