Skip to content

Auto-scaler: bare-metal backend

The bare-metal backend provisions agents as host child processes (child_process.spawn). Use it for workloads that cannot run in containers (GPU access, specialized hardware) or when container overhead is unacceptable. For fields shared across all backends, see Common configuration.

Label-set-level fields:

  • binaryPath — Filesystem path to the agent binary. The scaler spawns this process for each job.
  • image — A kici-agent container image. It has two uses, described in Container jobs below.

Every bare-metal label set needs at least one of the two. A set with only binaryPath spawns a host process, which is the classic bare-metal pool.

A job may name its own container image with the container field. KiCI runs such a job with its own Node build mounted read-only, so the image needs neither Node nor git. A bare-metal agent is a plain host process and carries no such build, so the label set’s image is where it comes from:

  • binaryPath and image — The pool spawns the agent process as usual. When that agent takes a container job, it starts the job’s container and copies the Node build out of image into a named volume, which it then mounts into the job container. The copy runs once per agent image on that host and is reused afterwards.
  • image only — The pool runs the job’s own image as the agent, with the same Node build mounted in. There is no host process. Use this for a pool that only ever runs container jobs. The agent runs inside the job’s image here, so that image must also ship git and bash; the agent refuses to start without them.
  • binaryPath only — No Node build is available to inject, so a container job runs on the image’s own node. That works for an image that ships one, such as node:24-slim.

The host needs docker or podman for any of this. See Container jobs for the job-side contract.

Scaler-level field:

  • enforceCgroups — When true, wrap each agent in a transient systemd-run --user --scope --slice=kici-scaler with CPUQuota= / MemoryMax= derived from the resolved resource limits. Default: false (advisory limits only). Linux-only; on macOS / Windows the flag silently no-ops with a startup warning. See cgroup enforcement.

Processes are spawned in detached process groups ({ detached: true }) to enable clean killing of entire process trees. Environment variables are passed directly to the spawned process:

  • KICI_ORCHESTRATOR_URL — Orchestrator WebSocket URL
  • KICI_AGENT_ID — Pre-generated agent ID for correlation
  • KICI_LABELS — Comma-separated label set
  • KICI_SCALER_MANAGED=1 — Scaler-managed flag
  • KICI_EXECUTION_MODE=bare-metal — Execution mode
  • KICI_PORT=0 — Random port assignment
  • KICI_RUNTIME_IMAGE — (set only when the label set declares an image) Where the agent materializes the KiCI runtime from when it nests a job container. A label set with binaryPath alone leaves it unset, which is why a container job on such a pool runs on the image’s own node
  • KICI_AGENT_TOKEN — (optional) Ephemeral auth token when auth is configured
  • KICI_BACKPRESSURE_MODE — (optional) Log backpressure mode from label set config
  • Host variables forwarded through the KICI_AGENT_ENV_ prefix
  • Any additional env entries from the label set config, which win on a conflict

All bare-metal agents are single-use: the agent process is spawned for one job, then killed after the job completes or the agent disconnects. Process group kill sequence: SIGTERM, wait 5s, SIGKILL.

By default, bare-metal resource limits are advisory — they drive the cap math (per-scaler / global / machine-pool budgets) but no cgroup is created. Set enforceCgroups: true on the scaler entry to wrap each agent in a transient systemd-run --user --scope --slice=kici-scaler with CPUQuota= / MemoryMax= derived from the resolved limits. This is Linux-only; on macOS and Windows the flag silently no-ops with a startup warning. The requests/limits model is described in Common configuration → Resource limits.

enforceCgroups governs process mode only. A container-mode agent (image only) takes its ceilings from the container runtime instead — memory, CPU and a process cap, exactly as the container backend applies them.

What isolates an agent depends on how its label set launches it.

  • Process mode (binaryPath) has no network isolation. The agent runs as a child process with full host filesystem and network access. A networkPolicy on such a label set is not enforced, and a startup warning names it. Use this mode in trusted environments only.
  • Container mode (image only — see Container jobs) joins the agent container to the isolated kici-agent-net network and applies the same per-address nftables rules as the container backend: the RFC1918 and cloud-metadata drops, plus the label set’s networkPolicy.

See Agent execution security for the isolation trade-offs across backends.

Remote orchestrator configuration (macOS / Windows)

Section titled “Remote orchestrator configuration (macOS / Windows)”

When running a multi-orchestrator cluster, remote Mac or Windows machines need bare-metal scaler entries to advertise their capabilities to the cluster. Without scaler config, the remote orchestrator’s heartbeats will show empty capabilities, and the cluster coordinator won’t route jobs to it.

  1. The remote orchestrator connects to the Platform relay as a peer in the cluster.
  2. On connection (and via periodic heartbeats), it advertises its scaler capacity — including the label sets it can handle and available concurrency.
  3. The cluster coordinator uses this advertised capacity to make informed routing decisions: when a job needs runsOn: ['macos'], it checks which peers have matching labels with available capacity.
  4. If no peer handles the required labels, the coordinator returns a clear error: “No orchestrator in cluster handles labels: macos”. If peers exist but are at capacity, it says: “Peers with matching labels exist but are at capacity”.
# scalers.yaml on the Mac orchestrator
version: 1
scalers:
- name: macos-bare-metal
type: bare-metal
maxAgents: 2
labelSets:
- labels: [macos, darwin, bare-metal]
binaryPath: /Users/youruser/kici/agent/kici-agent
# scalers.yaml on the Windows orchestrator
version: 1
scalers:
- name: windows-bare-metal
type: bare-metal
maxAgents: 2
labelSets:
- labels: [windows, bare-metal]
binaryPath: C:\kici\agent\kici-agent.exe

For a non-Linux bare-metal pool, prefer declaring the structured platform: { os, arch } field. It is the canonical way to taint a Windows / macOS / ARM pool so unqualified Linux jobs are never routed to it, and it works even when the pool’s plain labels use a non-canonical name. See Automatic platform taint in the common config reference.

  • Warm pool support: Bare-metal scalers accept a warmPool block and keep its agents ready like every other backend (see Warm pool). Starting a bare-metal process takes seconds, so a warm pool saves little here — the default size: 0 is the right choice for most bare-metal pools. The maxAgents field controls maximum concurrency (how many simultaneous jobs can run).

  • Intermittent availability: Remote orchestrators (especially developer laptops) may be intermittently available. When the machine is off or disconnected, jobs requiring its labels will fail with a clear error message (“No orchestrator in cluster handles labels: …”). This is expected behavior — the cluster coordinator handles it gracefully.

  • Capability advertisement is automatic: Once the scaler config is in place and the orchestrator is running, it automatically advertises its capabilities via heartbeats. No additional configuration is needed on the coordinator side.

  • Label matching: Jobs use runsOn label sets (e.g., runsOn: ['macos', 'arm64']). The coordinator matches these against the labelSets in each scaler’s config. All labels in the job’s runsOn must be present in the scaler’s label set for a match.

version: 1
globalMaxAgents: 5
scalers:
- name: gpu-machines
type: bare-metal
maxAgents: 3
labelSets:
- labels: ['linux', 'gpu', 'cuda']
binaryPath: '/opt/kici/kici-agent'
resources:
memory: '16g'
cpus: 8