Runner Provisioner Configuration Reference
Every Shipfox runner provisioner environment variable with its default, plus the runner template file schema: vars, defaults, matrix families, labels, image, cpu, memory, cost, and max_concurrency.
This page is the canonical reference for configuring the Docker runner provisioner (@shipfox/provisioner-docker). For how a provisioner works, see Runner Provisioners; for starting one, see Run a provisioner.
Required variables
| Variable | Purpose |
|---|---|
SHIPFOX_API_URL | Base URL of the Shipfox API the provisioner connects to. |
SHIPFOX_PROVISIONER_TOKEN | Long-lived token that authenticates control-plane calls. Create it for the provisioner scope you intend to run and keep it secret. The value is shown once and can be revoked. |
SHIPFOX_PROVISIONER_TEMPLATES_FILE | Path to the YAML file describing the runner templates this provisioner can start. |
Optional variables
| Variable | Default | Purpose |
|---|---|---|
SHIPFOX_RUNNER_API_URL | SHIPFOX_API_URL | Base URL injected into runner containers as their SHIPFOX_API_URL. Set it when containers reach the API through a different address than the provisioner uses. |
SHIPFOX_RUNNER_POLL_MAX_DURATION_MS | 300000 | Injected into each runner as SHIPFOX_POLL_MAX_DURATION_MS: how long a started runner polls for a job before exiting. Use 0 to poll forever. |
SHIPFOX_PROVISIONER_DOCKER_HOST | local socket | Docker daemon host. Set a Docker host URL when the daemon is remote. |
SHIPFOX_PROVISIONER_DOCKER_NETWORK | unset | Docker network attached to runner containers, for reaching the API through a Compose or bridge network. |
SHIPFOX_PROVISIONER_DOCKER_EXTRA_HOSTS | unset | Comma-separated host mappings added to runner containers, such as host.docker.internal:host-gateway. |
SHIPFOX_PROVISIONER_DOCKER_LOG_DRIVER | Docker daemon default | Logging driver for runner containers. Set this to override the daemon default for this provisioner. |
SHIPFOX_PROVISIONER_DOCKER_LOG_OPTIONS | unset | JSON object of string-valued logging-driver options. Requires SHIPFOX_PROVISIONER_DOCKER_LOG_DRIVER; option values are never written to provisioner logs. |
SHIPFOX_PROVISIONER_POLL_WAIT_SECONDS | 30 | How long each demand poll waits for work before returning. 0 makes the poll non-blocking. |
SHIPFOX_PROVISIONER_POLL_INTERVAL_MS | 1000 | Wait between demand polls. Backs off toward the max after errors. |
SHIPFOX_PROVISIONER_POLL_MAX_INTERVAL_MS | 5000 | Largest poll backoff interval after repeated errors. |
SHIPFOX_PROVISIONER_CONVERGE_INTERVAL_MS | 1000 | Provider observation and reconciliation cadence. Backs off on errors up to the larger of 5000 ms and the configured cadence. |
SHIPFOX_PROVISIONER_MAX_RESERVATIONS | 250 | Most reservations requested in one poll. Also bounded by free template capacity and the API's cap of 1000. |
SHIPFOX_PROVISIONER_RUNNER_INSTANCE_BATCH_SIZE | 250 | Runner instances created per request (1 to 1000). Must not exceed the API's own batch limit. |
SHIPFOX_PROVISIONER_REGISTRATION_DEADLINE_MS | 120000 | How long a created container may stay unstarted before the provisioner reaps it as stale. |
SHIPFOX_PROVISIONER_DOCKER_FAILED_CONTAINER_RETENTION_MS | 3600000 | How long failed runner containers remain stopped for forensic inspection. Set to 0 to disable retention. |
SHIPFOX_PROVISIONER_DOCKER_MAX_RETAINED_FAILED_CONTAINERS | 20 | Maximum number of failed runner containers retained per provisioner. Set to 0 to disable retention. |
SHIPFOX_RUNNER_MAX_LIFETIME_SECONDS | 3600 | Hard maximum lifetime injected into each runner. Set it above the longest permitted job so a runner terminates if its provisioner is unavailable. |
Docker logging and failed-container forensics
Provisioner logs and runner-container output are separate streams. The
provisioner never copies runner stdout or stderr into its own logs. Use
LOG_LEVEL, LOG_PRETTY, LOG_STDOUT, LOG_STDOUT_LEVEL, LOG_FILE, and
LOG_FILE_LEVEL for provisioner-process logs. LOG_FILE writes a plain file
without built-in rotation, so configure external rotation such as logrotate.
The logger keeps the file open: use copytruncate in the rotation rule or
restart the provisioner after rename/create rotation, otherwise it continues
writing to the old inode. For production, prefer stdout with journald or
runtime-managed rotation.
Runner containers inherit the Docker daemon logging driver unless
SHIPFOX_PROVISIONER_DOCKER_LOG_DRIVER is set. A local driver with on-host
rotation can be configured as follows:
export SHIPFOX_PROVISIONER_DOCKER_LOG_DRIVER=local
export SHIPFOX_PROVISIONER_DOCKER_LOG_OPTIONS='{"max-size":"10m","max-file":"5"}'For journald, configure the driver and a tag, then retrieve output with
journalctl:
export SHIPFOX_PROVISIONER_DOCKER_LOG_DRIVER=journald
export SHIPFOX_PROVISIONER_DOCKER_LOG_OPTIONS='{"tag":"shipfox-runner/{{.Name}}"}'
journalctl CONTAINER_NAME=<runner-name> --since todayEnable persistent journald storage (Storage=persistent in
/etc/systemd/journald.conf) and configure its disk and rotation limits.
Remote drivers use the retention and access policy of their durable backend.
For local and json-file, docker logs --timestamps --tail 200 <runner-name> works only until cleanup removes the container. Use journald or
the remote backend after cleanup.
Failed containers remain for forensic inspection only when both retention
settings are greater than zero. The default TTL is one hour and the default
count bound is 20. Docker FinishedAt drives TTL cleanup. Missing FinishedAt
uses the first observed failure time instead of creation time, so a long-running
container is not removed immediately. Terminal inspection failures defer TTL
cleanup but remain subject to the count bound, and unknown failure times rank as
newly observed for count eviction. Successful exits, dead containers,
stale-created containers, and backend termination keep their immediate cleanup
behavior.
Forensic output is driver-specific: use docker inspect <runner-name> and
docker logs --timestamps --tail 200 <runner-name> for local or json-file,
journalctl CONTAINER_NAME=<runner-name> for journald, and the configured
durable backend for remote drivers. For a remote Docker daemon, set
DOCKER_HOST to the configured SHIPFOX_PROVISIONER_DOCKER_HOST value before
running Docker commands; omit it for the local default. Run journalctl on the
Docker daemon host, not on the operator workstation. The none driver has no
container output.
Template file schema
The templates file can contain shared lookup data, a default fragment, hand-written one-offs, and any number of independent matrix families. This is a complete Docker example with a general fleet, a separate GPU family, and one hand-written template:
vars:
image_by_os:
ubuntu22: ghcr.io/shipfoxhq/runner:ubuntu22
ubuntu24: ghcr.io/shipfoxhq/runner:ubuntu24
defaults:
labels: [docker]
target_concurrency: 0
templates:
docker-local-debug:
labels: [docker, local-debug]
image: ghcr.io/shipfoxhq/runner:debug
cpu: 1
memory: 2GiB
max_concurrency: 2
cost: 1
matrix:
general:
axes:
os: [ubuntu22, ubuntu24]
cpu: [2, 4]
template:
labels: [docker, "${{ os }}", "${{ cpu }}vcpu"]
image: "${{ vars.image_by_os[os] }}"
cpu: "${{ cpu }}"
memory: "${{ cpu * 2.0 }}GiB"
max_concurrency: 50
cost: "${{ cpu }}"
gpu:
axes:
cuda: [cuda12, cuda13]
memory: [16g, 32g]
template:
labels: [docker, gpu, "${{ cuda }}", "${{ memory }}"]
image: "ghcr.io/shipfoxhq/runner:${{ cuda }}"
cpu: 8
memory: "${{ memory }}"
max_concurrency: 10
cost: 20The checked-in Docker and EC2 files at apps/provisioner-docker/templates.example.yaml
and apps/provisioner-ec2/templates.example.yaml use the same shape. Copy one, replace
the placeholder images, AMI IDs, subnets, and security groups, then point
SHIPFOX_PROVISIONER_TEMPLATES_FILE at the copy.
| Top-level key | Type | Description |
|---|---|---|
vars | map | Shared lookup data available to every family. EC2 operators use it for their own AMI and instance-type maps. |
defaults | map | A fragment merged under every hand-written and generated template. Maps merge recursively; lists and scalars replace wholesale. |
templates | map | Hand-written entries for genuine one-offs or per-variant overrides. A hand-written key shadows a generated key and emits a warning. |
matrix | map | Independent named families. Each family has its own axes and produces its own generated keys. |
Matrix families
| Key | Type | Description |
|---|---|---|
axes | map | Named axes whose values are crossed. An axis can be a list or an expression that returns a list. The default key includes every axis in declaration order. |
exclude | map list | Partial bindings to remove from the cartesian product. |
include | map list | Complete bindings to append as extra variants. Every declared axis must be present. |
let | map | Per-variant expressions evaluated in declaration order. Each binding can use axes, vars, and earlier bindings. |
key | expression | Optional explicit key. Without one, the loader builds a key from the family name and each axis value (or object axis name) in declaration order, joining parts with hyphens and escaping separators inside values; generated keys must be unique within the file. Use an explicit key only when you need a different naming scheme. |
template | map | Provider-specific template fields rendered for each variant. |
Families are independent. Adding a GPU pool means adding a gpu block with GPU
axes; it does not mean adding GPU axes to the general fleet. Labels may overlap across
families. Matching is subset-based, and the lowest cost wins when several templates
can serve the same job, followed by specificity.
Defaults do not append lists. If defaults.subnets contains two general subnets and
a GPU family sets subnets: [subnet-gpu], the GPU template has only subnet-gpu.
Use a block override whenever a family needs a different list or scalar.
The provider validates every rendered template with a strict schema. For Docker,
unknown top-level or template keys now fail with a file-scoped error. This is a
breaking upgrade for existing Docker files that relied on mistyped keys being ignored;
remove those keys before upgrading. The optional Docker cost field defaults to cpu.
| Field | Type | Description |
|---|---|---|
labels | string[] | Labels the started runner registers with. A template can serve any job whose required labels are all in this list. |
image | string | Optional container image to start. Defaults to ghcr.io/shipfoxhq/runner:latest. |
cpu | number | vCPUs allocated to the container. The default Docker selection cost when cost is omitted. |
memory | string | Memory allocation, such as 4GiB. |
max_concurrency | number | Cap on live containers started from this template. |
target_concurrency | number | Enrolled, unassigned runners to keep ready without demand. Defaults to 0. Keep it at or below max_concurrency: the provisioner accepts a template where it is higher. |
cost | number | Positive selection cost. Lower costs are preferred when several templates satisfy the same labels. Docker defaults it to cpu. |
Template selection: when several templates satisfy a job's labels, the provisioner
picks the lowest cost (or cpu for Docker when cost is omitted), then the most
specific. Use a hand-written entry under templates: to override one generated key;
the hand-written value wins and the loader logs the shadowed variant.
Related pages
Runner Provisioners
The control loop: reserve, create runner instances, launch, enroll, and assign.
Run a provisioner
Prerequisites, token creation, and the launch command.
Runner
The variables injected into each provisioned runner.