Runners
Container runners are optional features that allow steps to execute inside Docker containers or Kubernetes pods instead of directly on the worker host.
Runner modes
Section titled “Runner modes”| Runner | How it works |
|---|---|
local (default) |
Step runs directly on the worker host |
docker |
Step runs in a Docker container with workspace at /workspace |
pod |
Step runs as a Kubernetes pod with workspace via init container |
Input files
Section titled “Input files”When a job has file inputs, every runner step receives every file of the job, read-only, at {root}/{file_id}/{safe_name}:
| Runner | Root |
|---|---|
local |
{temp}/stroem-inputs-{uuid}/in, new for each claim and removed after the step. Its parent stroem-inputs-{uuid} has mode 0700; the files are 0444 and their directories 0555 |
docker, type: docker |
/inputs, bind-mounted read-only (/inputs:ro) in both workspace and no-workspace modes |
pod, type: pod |
/inputs, an emptyDir volume stroem-inputs mounted read-only. The init container stroem-inputs-init downloads the files and checks size and SHA-256 before the main container starts |
A runner: docker step in a Helm deployment with the dind sidecar sees an empty /inputs, because the sidecar does not share the worker’s file system. See Limitations.
Docker runner
Section titled “Docker runner”Prerequisites
Section titled “Prerequisites”- Build the worker with the
dockerfeature:Terminal window cargo build -p stroem-worker --features docker - Add
docker: {}to the worker config - The worker needs Docker daemon access (local socket, or DinD sidecar in K8s)
Worker config
Section titled “Worker config”capabilities: - script - dockerdocker: {}runner_image: "ghcr.io/fremvaerk/stroem-runner:latest"Kubernetes runner
Section titled “Kubernetes runner”Prerequisites
Section titled “Prerequisites”- Build the worker with the
kubernetesfeature:Terminal window cargo build -p stroem-worker --features kubernetes - Add a
kubernetes:section to the worker config - The worker needs in-cluster credentials or a kubeconfig with permissions to create/get/delete pods
- The server must be reachable from inside the pod (the init container downloads workspace tarballs)
The init container downloads the workspace at the step’s revision: the job’s
revision, or the commit a ref: step is pinned to — the
same files a local or docker step gets. While a server replica answers
503 (it is still loading a pinned commit), it tries again every 5 seconds,
up to 12 attempts. If the revision can no longer be served (a commit
force-pushed away, or a folder workspace that changed since the job was
created, on a replica that did not cache the old revision), the init
container fails and so does the step; it never runs another revision’s files.
This holds as long as the action’s manifest does
not replace the workspace-init container’s command: an override of it
is merged in as written, and Strøm cannot add the revision to it.
Worker config
Section titled “Worker config”capabilities: - script - kuberneteskubernetes: namespace: stroem-jobs init_image: curlimages/curl:latest # optional, defaultCombined config
Section titled “Combined config”Both features can be enabled simultaneously:
server_url: "http://stroem-server:8080"worker_token: "your-token"worker_name: "worker-1"max_concurrent: 4poll_interval_secs: 2workspace_cache_dir: /var/stroem/workspace-cachecapabilities: - script - docker - kubernetesrunner_image: "ghcr.io/fremvaerk/stroem-runner:latest"docker: {}kubernetes: namespace: stroem-jobsWorker capabilities and tags
Section titled “Worker capabilities and tags”Workers have three routing knobs (migration 041 split capability from tags; migration 042 flipped tags from taint to affinity semantics and added exclusive):
capabilities— what runners the worker supports. Any of"script","docker","kubernetes","agent". Every step derives a required ability from its action type and runner; that ability must appear in the worker’scapabilities. This axis is required.tags— free-form affinity labels. A step’stagslist must be a subset of the worker’stagsfor it to reach the worker. Empty = no affinity axis; the worker matches only steps that request no tags (unless combined withexclusive, see below).exclusive— bool (defaultfalse). Whentrue, the worker also refuses any step whosetagsdon’t cover all of the worker’s own tags. This restores the pre-042 “reserved worker” behaviour: atags: ["claude"], exclusive: trueworker accepts only steps that ask for"claude".
| Action | Runner | Required ability |
|---|---|---|
script |
local (default) |
script |
script |
docker |
docker |
script |
pod |
kubernetes |
docker |
— | docker |
pod |
— | kubernetes |
agent |
— | agent |
task, approval |
— | — (server-dispatched) |
Reserving a worker for a specific step
Section titled “Reserving a worker for a specific step”Pin a specific job to a specific worker without letting other jobs leak onto it. Add a matching tag on both sides AND set exclusive: true on the worker:
# Worker configcapabilities: - scripttags: - batch-runner # this worker is reserved for batch jobsexclusive: true # refuse anything that doesn't request "batch-runner"# Action in workflow YAMLrun-batch-job: type: script tags: ["batch-runner"] # explicitly request the reserved worker script: | ...Untagged script steps never reach batch-runner; batch steps only reach batch-runner (assuming no other worker has that tag).
Without exclusive: true, the tagged worker would still opportunistically claim untagged steps — useful when the worker has spare capacity, but wrong for a dedicated machine.
Extra capability tags on the step
Section titled “Extra capability tags on the step”Tags on the action can also express fine-grained runtime requirements:
flow: train-model: action: train-gpu tags: ["gpu"]A step with type: script, runner: docker, tags: ["gpu"] requires ability docker AND affinity gpu. It reaches any docker-capable worker whose tags include "gpu" — a tags: ["gpu"] or tags: ["gpu", "trusted"] worker qualifies; a worker with tags: [] does NOT (affinity fails).
Unmatched steps
Section titled “Unmatched steps”If no active worker matches on both axes, the step remains ready but unclaimed. After unmatched_step_timeout_secs (default 30 seconds, configurable in server recovery settings), the step fails with error: "No active worker with required capability/tags to run this step".
Example scenario: A step needs ability kubernetes but all workers have ["script", "docker"]. After 30 seconds, the step fails. To fix: add a worker with capabilities: ["kubernetes"], or remove the runner: pod requirement.
Pre-installed tools
Section titled “Pre-installed tools”The official runner image (ghcr.io/fremvaerk/stroem-runner) and worker image ship with these tools pre-installed:
System tools
Section titled “System tools”| Tool | Description |
|---|---|
bash |
Default shell |
curl |
HTTP client |
git |
Version control |
jq |
JSON processor |
yq |
YAML/JSON/XML processor |
tar / gzip |
Archive and compression |
unzip |
ZIP extraction |
ssh |
OpenSSH client |
Secret management
Section titled “Secret management”| Tool | Description |
|---|---|
sops |
Mozilla SOPS — encrypted file editing |
vals |
Helmfile vals — multi-backend secret resolution |
Language runtimes
Section titled “Language runtimes”| Tool | Description |
|---|---|
uv |
Python package manager and runner |
bun |
JavaScript/TypeScript runtime |
Example — Python with dependencies:
actions: analyze: type: script runner: docker script: | uv pip install pandas requests --system uv run python /workspace/scripts/analyze.pyExample — TypeScript:
actions: generate-report: type: script runner: docker script: | cd /workspace bun install bun run scripts/report.tsHelm chart configuration
Section titled “Helm chart configuration”When deploying via Helm, use these values to configure runners:
# Enable Kubernetes runnerhelm install stroem ./helm/stroem \ --set worker.kubernetes.enabled=true \ --set worker.kubernetes.namespace=stroem-jobs
# Enable Docker runner via DinD sidecarhelm install stroem ./helm/stroem \ --set worker.dind.enabled=trueSee Helm / Kubernetes for full deployment details.