Skip to content

Configuration Reference

All configuration is optional. On a typical single-host fleet, no variables need to be set: repos are discovered from runner directories, groups are inferred from repo names, and the GitHub owner is read from gh. The variables below exist for when a default is wrong, or for advanced setups.

How to set values

Copy fleet.env.example to fleet.env in the fleet root and uncomment what you need:

cp fleet.env.example fleet.env

Precedence: fleet.env is sourced by the scripts and wins over environment variables already in the environment. If you pass FLEET_REPOS='a b' ./runs.sh, a FLEET_REPOS= line in fleet.env will override it. Comment the line out instead.

Dashboard settings (capacity, autoscaling) are stored in SQLite and editable from the UI. A value in fleet.env seeds the setting on first start but does not override it once it has been changed in the UI — the UI-stored value wins. The Capacity tab shows which layer (stored, env, or built-in default) each value came from.

Restart the daemon after changing fleet.env:

cd dashboard && ./fleetctl.sh restart

Shell script variables

Used by register.sh, health.sh, status.sh, cleanup.sh, runs.sh, preflight.sh and the hooks.

Variable Default What it does
FLEET_ROOT Script directory Fleet root directory. Set if the scripts live outside it.
FLEET_OWNER gh api user --jq .login GitHub user or org for bare repo names.
FLEET_REPOS All runner directories Explicit repo list for runs.sh.
FLEET_REPO_CACHE .fleet-repos Where runs.sh caches discovered repos.
FLEET_REPO_CACHE_TTL 21600 (6 h) How long the cache is valid, in seconds.
FLEET_REPO_LIMIT 200 Max repos discovery considers.
FLEET_LABEL_PREFIX com.runner-fleet LaunchAgent label prefix. Set before install; do not change while loaded.
RUNNER_INSTANCE 1 Instance number for register.sh. RUNNER_INSTANCE=2 adds a second runner.

Health repair

Used by healthctl.sh and scripts/health-repair-launchd.sh. See Operations → Scheduled maintenance.

Variable Default What it does
FLEET_HEALTH_INTERVAL 60 Seconds between health.sh --repair sweeps. Reinstall after changing: ./healthctl.sh install.
FLEET_HEALTH_LOG logs/fleet-health-repair.log under FLEET_ROOT Where the repair LaunchAgent writes stdout/stderr.
FLEET_HEALTH_LOCK .health-repair.lock.d under FLEET_ROOT Lock directory preventing overlapping sweeps.

The LaunchAgent label is <FLEET_LABEL_PREFIX>.fleet-health (default com.runner-fleet.fleet-health). Set FLEET_LABEL_PREFIX before ./healthctl.sh install; changing it later requires uninstall under the old label and reinstall.

Disk cleanup

Used by cleanup.sh and cleanupctl.sh. See Operations → Disk cleanup.

Variable Default What it does
FLEET_CLEANUP_PRESSURE_GB FLEET_ADMIT_MIN_FREE_DISK_GB + 20 Below this much usable disk, --auto acts on every tick and takes the pressure steps too.
FLEET_CLEANUP_RUNNER_CACHE_DAYS 7 Erase an idle runner's warm build caches (_work/<repo>/DerivedData-*, spm-*, .build-cache-*) unused this long; under pressure, all of them.
FLEET_CLEANUP_SIM_MAX_GB 3 Erase an idle runner's ci- simulator past this size.
FLEET_CLEANUP_SIM_PRESSURE_GB 1 ...and past this under pressure.
FLEET_CLEANUP_FULL_EVERY_S 86400 --auto runs a full pass at least this often.
FLEET_CLEANUP_AVD_KEEP_DAYS 14 Keep a runner's quick-boot snapshot AVD (ci-<runner>-a<api>-<target>-<profile>, the kit's emulator_snapshot) until it has gone this long without a boot.
FLEET_CLEANUP_AVD_KEEP_GB 30 The snapshot AVDs in all. Over it, even on a quiet --auto tick, idle runners' snapshots (snapshots/ inside the AVD) go, least recently used first; the AVD stays. Below the admission floor every idle snapshot goes, before any other step.
FLEET_CLEANUP_AVD_PREFIXES ci shots AVD name prefixes that mark a runner's own emulator. Nothing else is touched.
FLEET_CLEANUP_EMU_ORPHAN_MIN_S 300 Stop a CI emulator whose parent is launchd and whose runner runs no job once it is this old (on every tick).
FLEET_EMULATOR_CLEANUP 1 The job-completed hook stops an emulator the job left running on its runner's AVD (adb emu kill, then SIGTERM, SIGKILL last). 0 turns it off.
FLEET_CLEANUP_INTERVAL 900 Seconds between --auto ticks. Reinstall after changing: ./cleanupctl.sh install.
FLEET_CLEANUP_LOG ~/Library/Logs/runner-cleanup.log Where the cleanup LaunchAgent writes stdout/stderr.

The LaunchAgent label is <FLEET_LABEL_PREFIX>.runner-cleanup.

Grouping variables

Used by the dashboard and runs.sh to group repos.

Variable Default What it does
FLEET_PROJECTS (inferred) Force-create groups in this order, as prefixes.
FLEET_GROUP_IGNORE (empty) Never infer groups for these tokens.
FLEET_GROUP_MIN 2 Minimum repos sharing a prefix to form a group.
FLEET_GROUPS on Set to off to disable grouping entirely.

Dashboard daemon variables

Set in the environment of fleetd.js, typically via the LaunchAgent plist that fleetctl.sh install writes. To change after install:

  1. Edit fleet.env (if the variable is sourced from it), or
  2. Edit the plist: launchctl edit $(./fleetctl.sh label), then launchctl kickstart -k gui/$(id -u)/$(./fleetctl.sh label)
Variable Default What it does Restart required?
FLEET_PORT 7878 Dashboard HTTP port. Yes
FLEET_HOST 127.0.0.1 Bind address. 0.0.0.0 for LAN. See Federation. Yes
FLEET_ALLOWED_HOSTS local hostnames and interfaces Additional comma-separated DNS names or IPs accepted by the Host-header rebinding guard. Yes
FLEET_TAILSCALE auto Automatically discover Tailscale for reachable URLs and proxy context. Set off to disable discovery. Yes
FLEET_TAILSCALE_BIN auto-detected Explicit Tailscale CLI path, including the Mac App Store application binary when needed. Yes
FLEET_ROOT ~/actions-runners Fleet root. Yes
FLEET_DB dashboard/fleet.db SQLite database path. Yes
FLEET_TOKEN_FILE dashboard/.fleet-token Browser control token path. Yes
FLEET_AGENT_TOKEN_FILE dashboard/.fleet-agent-token Agent auth token file. Falls back to control token if absent. Yes
FLEET_AGENT_TOKEN (from file) Agent token value directly in env. Overrides the file. Yes
FLEET_AGENT_TOKENS_FILE dashboard/.fleet-agent-tokens.json Hashes for host-scoped agent tokens generated by fleetctl.sh agent-token --host. A scoped host must use its own token; unscoped hosts may keep the shared token during a staged rollout. Yes
FLEET_DEVICE_TOKENS_FILE dashboard/.fleet-device-tokens.json Mode-0600 hashes and metadata for individually revokable paired-browser tokens. Yes
FLEET_PUBLIC_PORT (off) Loopback port of the public MCP listener for chat tools. Set with FLEET_PUBLIC_URL by fleetctl.sh remote funnel on or remote cloudflare on. See Chat tools. Yes
FLEET_PUBLIC_URL (off) The https origin a tunnel publishes FLEET_PUBLIC_PORT at, used as the OAuth issuer. Yes
FLEET_OAUTH_CLIENTS_FILE dashboard/.fleet-oauth-clients.json Mode-0600 store of chat-tool clients registered through OAuth. Yes
FLEET_DATABASE_URL_FILE (unset) Mode-0600 file containing the managed PostgreSQL URL. Enables shared snapshots, commands, settings, and leader election. Yes
FLEET_REPLICA_ID short hostname Stable unique control-plane/host ID for this Mac. Yes
FLEET_HOST_NAME short hostname Human-readable host name shown in the dashboard. Yes
FLEET_DATABASE_SSL 1 Require TLS for PostgreSQL. Set 0 only for local development. Yes
FLEET_DATABASE_SSL_INSECURE 0 Disable PostgreSQL certificate verification. Use only for a private test database. Yes
FLEET_HOST_LABELS (empty) Comma-separated capability labels for the coordinator host (e.g. xcode-16,macos-15). Used by placement to match jobs to eligible hosts. Leaving it empty means the host advertises nothing, so placement refuses every scale-up that needs a label — see the note below. Yes
FLEET_READ_ONLY 0 Set to 1 to disable the control plane entirely. Yes
FLEET_HOLD_DRAINS 1 Stop a drained runner that launchd started again (login, reboot) within a tick, keeping its drain owner. 0 leaves it running. Also read by the agent. Yes
FLEET_FAST_MS 15000 Fast-loop interval while busy, ms. No
FLEET_IDLE_MS 45000 Fast-loop interval at rest, ms. No
FLEET_SLOW_MS 900000 (15 min) Slow-loop interval, ms. No
FLEET_ALERTS 1 Set to 0 to disable the alerts system. No
FLEET_ALERT_CONFIG dashboard/alerts.config.json Alert configuration file. No
FLEET_ADMISSION_LOG dashboard/logs/admission.ndjson Where job-started.sh appends decisions. Yes
FLEET_BACKFILL_MS 600000 (10 min) How often a backfill pass runs, until it has caught up. No
FLEET_BACKFILL_CALLS 350 API calls one pass may spend. Bounded on purpose: ~1,100 runs need ~1,100 job calls, and a greedy sweep starves the fast loop. No
FLEET_BACKFILL_FLOOR 1500 Rate-limit budget the backfill will not dip below, so the fast loop always has calls left. No
FLEET_REPO_FETCH_CONCURRENCY 6 Per fast tick: how many repos are read from GitHub at once (about five requests each: runners, runs, three active-run statuses). Unbounded, 39 repos was ~195 requests in flight, past GitHub's limit of 100 concurrent requests, and the 429 stopped every read. Yes
FLEET_SETTLE_CALLS 12 Per fast tick: re-fetches of jobs whose run just completed but whose row still has no conclusion (lib/settle.js). The backfill's settle phase catches anything over budget. No
FLEET_BACKUP_DIR dashboard/backups Destination directory for ./fleetctl.sh backup. No
FLEET_COLLECTOR_STALE_MS 240000 (4 min) Snapshot age after which /api/health reports stale: true and metrics mark the collector stale. No

Database backups and restore drills

The only file worth backing up regularly is fleet.db — it holds run history, admission events, alert state, and capacity settings. Token files and runner credentials are separate; back those up only if you need to preserve control access without regenerating tokens.

cd dashboard
./fleetctl.sh backup          # online, consistent, mode 0600
./fleetctl.sh stop
./fleetctl.sh restore backups/fleet-YYYYMMDD-HHMMSS.db
./fleetctl.sh start

restore preserves the current database as fleet.db.rollback-YYYYMMDD-HHMMSS before overwriting. Run this sequence on a schedule (monthly is enough for most fleets) and after every upgrade that touches schema migrations:

  1. ./fleetctl.sh backup
  2. ./fleetctl.sh stop && ./fleetctl.sh restore <that backup> && ./fleetctl.sh start
  3. Confirm ./fleetctl.sh status and that the Capacity tab loads
  4. Check migrations: sqlite3 fleet.db "SELECT id, applied_at FROM schema_migrations ORDER BY id DESC LIMIT 5;"

If restore fails, stop again and copy the rollback file back over fleet.db.

Prometheus metrics

dashboard/lib/metrics.js renders the live snapshot as Prometheus text format. Metrics cover collector age/staleness, queue depth and worst age by cause, runner totals and busy count, admission waits, local and fleet-wide headroom, autoscale state, and federation host counts.

fleetd exposes these at GET /metrics. Scrape from the same bind address as the dashboard (FLEET_HOST:FLEET_PORT/metrics) and treat it like /api/state: unauthenticated read access on loopback, review exposure on LAN bindings. See Security hardening.

Multi-host rollout

Federated fleets add agent hosts gradually. A safe rollout sequence:

  1. Coordinator first — ./install.sh coordinator, set FLEET_HOST_LABELS to what the coordinator can actually run, ./fleetctl.sh agent-token, back up fleet.db.
  2. One agent — ./install.sh agent with FLEET_COORDINATOR, matching FLEET_AGENT_TOKEN, and that host's FLEET_HOST_LABELS. Leave FLEET_AGENT_ALLOW_COMMANDS=0 until heartbeats appear in the Hosts tab.
  3. Dry-run autoscale — enable autoscale with dry-run on the Capacity tab; confirm placement picks the expected host before allowing live actions.
  4. Remote provisioning last — set FLEET_AGENT_ALLOW_REGISTER=1 only on hosts you intend to grow. Each agent re-checks local headroom before register.sh runs; a stale coordinator snapshot cannot override that.
  5. Remote removal separately — set FLEET_AGENT_ALLOW_DEREGISTER=1 only after drain/restart/register commands and their audit results have been exercised. Scale-down remains harmlessly refused without it.

Roll agents one at a time. Two agents with the same FLEET_HOST_NAME are confusing in the Hosts tab; two coordinators pointing at different fleet.db files split history permanently unless both use the PostgreSQL HA mode below.

Two-replica high availability

Both Macs may run jobs and fleetd, but only one process may collect GitHub state, autoscale, backfill, and reconcile alerts. Set the same managed PostgreSQL URL on both Macs through a mode-0600 FLEET_DATABASE_URL_FILE, and give them distinct FLEET_REPLICA_ID values. PostgreSQL advisory locking elects one leader; the standby serves the shared dashboard snapshot and automatically promotes if the leader connection disappears.

Do not also run dashboard/agent.js on either HA replica. Each fleetd reports that Mac and executes commands addressed to its own FLEET_REPLICA_ID; running the agent too would duplicate host heartbeats and command consumers.

printf '%s\n' 'postgresql://fleet:...@db.example/fleet?sslmode=require' \
  > dashboard/.fleet-database-url
chmod 600 dashboard/.fleet-database-url
FLEET_REPLICA_ID=mac-mini ./dashboard/fleetctl.sh install

Before starting the second replica, archive and verify the existing history:

cd dashboard
FLEET_DATABASE_URL="$(cat .fleet-database-url)" npm run migrate:postgres

Each replica keeps a local SQLite operational/history cache for low-overhead analytics; PostgreSQL is authoritative for leadership, current fleet snapshots, host heartbeats, host commands, settings, and autoscale cooldowns. The migration archive preserves every SQLite table in PostgreSQL JSONB with row counts and checksums.

Warm capacity and pre-warming limits

Adding runners increases concurrency, not CPU or memory. The headroom gate (FLEET_CEILING, FLEET_MAX_TOTAL_RUNNERS, load and disk checks) refuses new runners while the host is already busy — it does not throttle jobs already running. In federation, placement may put a new runner on a remote host with headroom even when the coordinator is saturated.

Forecast-based pre-warming remains off by default and shadow-gated: the Capacity tab shows burst patterns and cron schedules, but planPrewarm() refuses to act until precision and recall gates pass (see Capacity design). Turn Pre-warm on from the Capacity settings only after the gate shows green. Even when enabled, pre-warming is still subject to headroom, per-repo caps, lead time, per-repo cooldown, and the one-action-per-sweep rule — a predicted burst does not bypass those limits, and reactive autoscale still wins when work is already queued or running.

A note on FLEET_HOST_LABELS

Placement refuses any host that does not advertise every label a new runner needs. An unset value therefore does not mean "no constraints", it means the host advertises none and matches nothing, so every scale-up for a labelled runner is refused with missing required label(s): … — naming a label the machine may well have served for months. Scale-up is off by default, which is how an empty value goes unnoticed until the first time autoscale is switched on.

Set it to what the host can actually run. The safe way to derive it is from the runners already on the host: if a runner there carries ci and runs jobs with it, the host can serve ci. Do not add a label for something not installed — a host advertising xcode-27 without it does not fail at placement, it fails halfway through somebody's build.

Capacity and autoscaling

These live in SQLite and are editable from the Capacity tab without restarting the daemon. A value in fleet.env seeds the setting on first boot.

Setting Default What it does
FLEET_CEILING 3 Refuse to add runners while this many jobs are running.
FLEET_MAX_TOTAL_RUNNERS 8 Hard cap on runners per host.
FLEET_LOAD_PER_CORE 0.7 Refuse to add runners above this load-per-core ratio.
FLEET_MIN_FREE_DISK_GB 20 Refuse to add runners below this free-disk threshold.
Autoscale on/off off Toggle on Capacity tab.
Autoscale dry-run off Log decisions without acting; toggle on Capacity tab.
Provision unserved repos off Register a repo's first runner when it has self-hosted work queued and no runner at all. Everything else the autoscaler does is a duplicate, which is created by copying a sibling — a repo with no runner has no sibling, so this is a separate path. Requires FLEET_HOST_LABELS to be set, since it places a labelled runner. See Giving a repo its first runner.
Min queued (sustained) 600000 ms (10 min) How long work must wait before sustained-mode scale-up.
Scale-up cooldown (sustained) 1800000 ms (30 min) Minimum gap between sustained scale-ups for one repo+role.
Burst mode off Shorter thresholds when several jobs queue for one repo+role at once.
Burst min queued jobs 2 Queue depth before burst thresholds apply.
Burst min queued 120000 ms (2 min) Queue wait before burst-mode scale-up.
Burst cooldown 300000 ms (5 min) Minimum gap between burst scale-ups for one repo+role.
Burst max batch 2 Most runners to add in one revalidated batch (burst mode only).
Idle TTL 259200000 ms (3 days) Deregister a duplicate runner idle for this long. Three days rather than hours: a dry run at six hours proposed removing a duplicate that had run 103 jobs and last worked that morning — six hours does not mean unused, it means overnight. See Capacity and autoscaling.
Pre-warm off Add one duplicate ahead of a forecast burst. Requires the shadow gate to pass and obeys the same dry-run switch as autoscale.
Pre-warm lead 3600000 ms (1 h) Act only when the predicted window starts within this lead time.
Pre-warm cooldown 3600000 ms (1 h) Minimum gap between pre-warms for one repo.
Pre-warm min confidence medium Accept medium or high forecast confidence; set to high to require cron-backed or doubly-confirmed history.

Job admission variables

Used by hooks/job-started.sh. Must be in fleet.env — the hooks cannot reach the daemon's database.

fleetctl.sh install also passes FLEET_ADMIT_MODE, FLEET_ADMIT_MAX_WAIT_S, FLEET_ADMIT_MIN_FREE_DISK_GB, FLEET_ADMIT_MAX_CONCURRENT, FLEET_ADMIT_SIMULATOR_MAX_CONCURRENT and FLEET_SIMULATOR_RUNNERS to the daemon, so the fleet verdict, the admission-hold alert and the standby tiers judge the host by the limits the hooks actually enforce. After changing any of them, rerun ./fleetctl.sh install.

Variable Default What it does
FLEET_ADMIT_MODE off off / observe / enforce.
FLEET_ADMIT_MAX_CONCURRENT 3 Max concurrent jobs across the host.
FLEET_ADMIT_SIMULATOR_MAX_CONCURRENT 0 Strict max for runners matching FLEET_SIMULATOR_RUNNERS; 0 or no matching patterns disables the extra limit.
FLEET_ADMIT_MAX_WAIT_S 600 Max wait before FLEET_ADMIT_TIMEOUT_ACTION takes effect.
FLEET_ADMIT_TIMEOUT_ACTION admit admit fails open after max-wait for host and disk limits; the Simulator limit remains strict. hold keeps every limit strict until the run ends.
FLEET_ADMIT_CANCEL_POLL_S 30 How often a held hook checks whether GitHub has completed or cancelled its run.
FLEET_ADMIT_STATUS_URL http://127.0.0.1:${FLEET_PORT:-7878}/api/run-status Where a held hook asks for its run's status before trying gh and curl. Jobs run without the keychain, so the daemon's token is the one that works. Empty skips the daemon.
FLEET_ADMIT_MIN_FREE_DISK_GB 40 Refuse job (then admit after max wait) below this much usable disk: plain free plus what macOS can purge, as macOS reports it for important use.
FLEET_ADMIT_MIN_PLAIN_FREE_GB 15 Hard floor on plain df free, which a purge takes time to raise.
FLEET_DISK_PROBE (macOS probe) df makes both floors read plain df, ignoring purgeable space. Hosts without osascript always do.
FLEET_ADMIT_POLL_S 5 How often a held job re-checks.
FLEET_ADMIT_MUTEX_TRIES 50 Mutex spin attempts per wait-loop tick in enforce mode.
FLEET_ADMIT_SLOT_TTL_S 21600 Slot lease TTL in seconds.
FLEET_ADMIT_LANES off off / observe / enforce: admission lanes. observe keeps the single lane deciding and logs what the lanes would have decided.
FLEET_ADMIT_TOTAL_MAX_CONCURRENT FLEET_ADMIT_MAX_CONCURRENT The total cap the lanes apply.
FLEET_ADMIT_HEAVY_MAX_CONCURRENT 4 Heavy jobs at once; Simulator jobs count here too.
FLEET_ADMIT_LIGHT_MAX_CONCURRENT 12 Light jobs at once. FLEET_ADMIT_SHORT_MAX_CONCURRENT is accepted as an alias.
FLEET_ADMIT_HEAVY_PATTERNS (none) Space-separated repo:job shell patterns that are always heavy. job is GITHUB_JOB, the job's YAML id; repo may omit the owner. Checked before the light patterns.
FLEET_ADMIT_LIGHT_PATTERNS (none) The same, always light.
FLEET_ADMIT_SHORT_JOB_S 90 A job the patterns do not name is light when its recent median run (/api/job-class) is under this many seconds, with at least 5 runs; otherwise heavy.
FLEET_ADMIT_CLASS_CACHE_S 300 How long the hook reuses the daemon's answer for one repo and job.
FLEET_ADMIT_CLASS_URL /api/job-class beside FLEET_ADMIT_STATUS_URL Where the hook asks a job's class. Empty, or an empty FLEET_ADMIT_STATUS_URL, skips it: unnamed jobs are then heavy.

Eligible enforced waiters are FIFO. A Simulator waiter blocked by its resource-specific limit does not prevent an unrelated waiter from using free host capacity. A waiter is removed when its hook exits, and stale entries are reaped by hook-process liveness.

Standby tier variables

Read by the coordinator's daemon (dashboard/lib/tiers.js). Set them in the coordinator's fleet.env and rerun ./dashboard/fleetctl.sh install; the plist carries them, so a plain restart does not pick up a change. How the tiers work and why the timers are what they are: Standby tiers.

Variable Default What it does
FLEET_TIERS_MODE off off / observe / enforce. observe logs and shows what the controller would drain or resume and touches nothing. enforce acts. off stops deciding, and resumes any runner the controller had drained (never an operator's). Anything else reads as off.
FLEET_TIERS_PRIMARY_HOST the coordinator Host id whose runners take work first.
FLEET_TIERS_STANDBY_HOSTS (empty) Comma-separated agent host ids (FLEET_HOST_ID) whose runners are the floor and overflow tiers. Empty: no standby tier.
FLEET_TIERS_FLOOR_REPOS (empty) Repos (bare name or owner/repo) whose standby-host runners stay online always. The controller never drains them.
FLEET_TIERS_PRIMARY_ONLY_REPOS (empty) Repos whose primary runners are never self-drained, for work the standby host must not take (a label it lacks on purpose). A runner without an online standby twin is never self-drained anyway.
FLEET_TIERS_RESUME_AFTER_S 60 Resume the overflow tier when a primary lane has been at its admission cap this long.
FLEET_TIERS_QUEUE_AGE_S 120 ...or when a queued run has waited this long with no idle primary runner that matches it.
FLEET_TIERS_DRAIN_AFTER_S 600 Drain the overflow tier again after this long with every primary lane below cap, nothing queued and no unfit verdict rung.
FLEET_TIERS_PRIMARY_STALE_S 180 The primary counts as down when its heartbeat is older than this (a remote primary only).
FLEET_TIERS_SELF_DRAIN 1 0 turns off primary self-drain (and resumes runners it drained).
FLEET_TIERS_PARK 1 Self-drain a primary runner with no standby twin too ("park" it), so its jobs wait in GitHub's queue instead of in the admission hook, where the wait counts against timeout-minutes; parked runners come back one per free lane slot, oldest queued run first. 0 keeps a twinless runner online at cap. See tiers.
FLEET_TIERS_SELF_DRAIN_AFTER_S 60 A primary lane starts self-draining once it has been at its cap this long.
FLEET_TIERS_SELF_RESUME_AFTER_S 300 Self-drained primary runners come back once their lane has had room this long...
FLEET_TIERS_SELF_MIN_HOLD_S 600 ...and not before the lane has been self-draining this long. Resumes that keep a repo served (no twin, primary-only, self-drain off) never wait.
FLEET_TIERS_SELF_MIN_OPEN_S 300 A lane does not start self-draining again sooner than this after its runners came back.
FLEET_TIERS_ADMIT_RESUME_AFTER_S 300 A host whose admission hook would hold every job (disk under its own FLEET_ADMIT_MIN_FREE_DISK_GB or FLEET_ADMIT_MIN_PLAIN_FREE_GB) has all its runners drained. It gets them back once it has been able to admit this long.
FLEET_TIERS_BUILD_CAP FLEET_ADMIT_MAX_CONCURRENT The primary's build-lane cap, if it differs from the admission setting fleetctl.sh passes through.
FLEET_TIERS_SIMULATOR_CAP FLEET_ADMIT_SIMULATOR_MAX_CONCURRENT The primary's Simulator-lane cap; runners in the lane are the ones FLEET_SIMULATOR_RUNNERS matches. 0: no Simulator lane.

The standby host's agent needs FLEET_AGENT_ALLOW_COMMANDS=1 to carry out tiers.drain and tiers.resume; FLEET_AGENT_COMMANDS can narrow it to drain and resume only.

Job audio variable

Used by both job hooks. Apple simulators route app audio through the host and do not expose a supported per-simulator mute.

Variable Default What it does
FLEET_MUTE_RUNNERS empty Space- or comma-separated exact runner names whose jobs mute host output. The original mute state is restored after the last selected job, including when a worker exits without running its completed hook.

Simulator cleanup variables

Used by both job hooks. Cleanup is opt-in because simulators are shared with interactive Xcode sessions on hosts that are also developer workstations.

Variable Default What it does
FLEET_SIMULATOR_CLEANUP 0 Set to 1 to enable job-scoped simulator shutdown.
FLEET_SIMULATOR_RUNNERS empty Space- or comma-separated runner-name shell patterns whose jobs use Apple simulators. After the final overlapping selected job ends, devices created since the first job began are shut down; devices already booted beforehand are preserved. A guardian also cleans up if the worker exits without its completed hook.

Shared memory variables

Used by both job hooks through hooks/shm-reap.sh. A Postgres cluster that a cancelled or timed-out job kills keeps its System V shared memory segment, and macOS caps the host at kern.sysv.shmmni = 32 of them, so after about thirty such jobs every new cluster fails with could not create shared memory segment: No space left on device. The sweep removes only segments owned by the runner user with no process attached (NATTCH 0) whose creator PID is no longer running; anything attached, such as a Homebrew Postgres service, is never touched.

Variable Default What it does
FLEET_SHM_REAP 1 Set to 0 to turn the sweep off.
FLEET_SHM_LOG dashboard/logs/shm.ndjson NDJSON log: one reaped line per sweep that removed something (with the count and segment ids), and a low line whenever fewer than FLEET_SHM_LOW_FREE segments are left free. A clean sweep writes nothing.
FLEET_SHM_LOW_FREE 4 Free-segment count below which the sweep logs low and warns in the job log, even when nothing could be removed. The dashboard opens the shm-low verdict below the same count.

The dashboard reads the same table. fleetd.js and agent.js sample ipcs -m -o -p every tick, counting orphans by the hook's rules as free, and summarise the tail of FLEET_SHM_LOG: segments reaped in the last 24 hours (the leak rate, shown on the host card) and the newest low event. Both ride on the host vitals, so an agent host's table reaches the coordinator in its heartbeat. The verdict's shm-low rung opens when the live sample is under FLEET_SHM_LOW_FREE, or a low event is under 30 minutes old; the alert of the same name opens with it. fleetctl.sh install and agentctl.sh install pass FLEET_SHM_LOG and FLEET_SHM_LOW_FREE from fleet.env into the LaunchAgent, so the daemon and the hooks agree on both.

Agent variables

Used by dashboard/agent.js on agent Macs. Set in fleet.env on the agent host and referenced from the LaunchAgent plist written by agentctl.sh install.

Variable Default What it does
FLEET_COORDINATOR (required) Coordinator URL, e.g. http://mac-main:7878.
FLEET_COORDINATORS (unset) Comma-separated coordinator URLs tried in order. Use both fleetd replicas in HA mode.
FLEET_AGENT_TOKEN (required) Host-scoped token generated with ./dashboard/fleetctl.sh agent-token --host <FLEET_HOST_ID> on the coordinator.
FLEET_HOST_ID FLEET_HOST_NAME Stable command-routing and token-scope ID. Do not change it when renaming the display name.
FLEET_HOST_NAME $(hostname) Display name shown in the Hosts tab.
FLEET_ROOT ~/actions-runners Fleet root on this agent host.
FLEET_HOST_LABELS (empty) Comma-separated capability labels for placement matching (e.g. xcode-16,macos-15).
FLEET_AGENT_ALLOW_COMMANDS 0 Set to 1 to allow remote drain/resume/health-check.
FLEET_AGENT_ALLOW_REGISTER 0 Set to 1 to allow remote runner registration (requires FLEET_AGENT_ALLOW_COMMANDS=1). The agent re-checks local headroom and the per-repo cap before invoking register.sh.
FLEET_AGENT_ALLOW_DEREGISTER 0 Separate destructive opt-in for remote removal and scale-down.
FLEET_AGENT_COMMANDS (empty) Optional allowlist on top of FLEET_AGENT_ALLOW_COMMANDS, comma-separated action names. runner.drain,runner.resume,tiers.drain,tiers.resume grants drain and resume and nothing else (no restarts, no health repair). Empty: every remote action the agent knows.
FLEET_HOLD_DRAINS 1 Stop a drained runner that launchd started again (login, reboot) within a heartbeat. 0 leaves it running.
FLEET_AGENT_AUTONOMY_S 180 Standby tiers: after this long without a successful heartbeat, resume every runner on this host the tiers controller drained (.drain marker by=tiers), never an operator's. Control returns to the coordinator when heartbeats succeed again. 0 turns it off.
FLEET_MAX_INSTANCES_PER_REPO 4 Agent-side per-repository cap; keep aligned with the Capacity setting.
FLEET_MIRROR_SKIP_REPOS (empty) Repos scripts/mirror-runners.sh never copies to this host, for jobs whose needs no label states (an iOS lane on a plain ci runner, a suite that expects a local database). Bare names or owner/repo, space- or comma-separated.
FLEET_XCODE_VERSION (unset) The Xcode every runner host builds with, e.g. 27.0. scripts/join-host.sh fails preflight when this host's xcodebuild -version differs, because with runners on two hosts a job's SDK would otherwise depend on which host GitHub picked.
FLEET_RUNNER_PATH /opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin The PATH every job gets. register.sh writes it to each runner's .path (which config.sh would otherwise fill from the registering shell) and .env; scripts/runner-path.sh aligns older runners. Keep it the same on every host.
FLEET_MAX_TOTAL_RUNNERS 8 Agent-side runner cap, sent to coordinator for placement scoring.
FLEET_CEILING 3 Agent-side busy-job ceiling (do not add runners while this many jobs run).
FLEET_LOAD_PER_CORE 2 Agent-side load gate.
FLEET_MIN_FREE_DISK_GB 50 Agent-side disk gate.
FLEET_HEARTBEAT_MS 30000 Heartbeat interval, ms.
FLEET_ADMISSION_LOG <FLEET_ROOT>/dashboard/logs/admission.ndjson The hooks' admission log the agent ships to the coordinator. Same default as hooks/common.sh.
FLEET_AGENT_ADMISSION_OFFSET_FILE dashboard/.fleet-agent-admission-offset How far the coordinator has accepted that log. Created at the log's end on first start; seed it to continue an existing history (see Federation).

Secret variables

These should never appear in fleet.env or any tracked file. Pass them via the LaunchAgent environment or a keychain-backed wrapper.

Variable Used by
GITHUB_TOKEN register.sh, if set (otherwise uses gh CLI)
FLEET_AGENT_TOKEN Agent authentication — treat as a credential