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:
Precedence:
fleet.envis sourced by the scripts and wins over environment variables already in the environment. If you passFLEET_REPOS='a b' ./runs.sh, aFLEET_REPOS=line infleet.envwill 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:
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:
- Edit
fleet.env(if the variable is sourced from it), or - Edit the plist:
launchctl edit $(./fleetctl.sh label), thenlaunchctl 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:
./fleetctl.sh backup./fleetctl.sh stop && ./fleetctl.sh restore <that backup> && ./fleetctl.sh start- Confirm
./fleetctl.sh statusand that the Capacity tab loads - 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:
- Coordinator first —
./install.sh coordinator, setFLEET_HOST_LABELSto what the coordinator can actually run,./fleetctl.sh agent-token, back upfleet.db. - One agent —
./install.sh agentwithFLEET_COORDINATOR, matchingFLEET_AGENT_TOKEN, and that host'sFLEET_HOST_LABELS. LeaveFLEET_AGENT_ALLOW_COMMANDS=0until heartbeats appear in the Hosts tab. - Dry-run autoscale — enable autoscale with dry-run on the Capacity tab; confirm placement picks the expected host before allowing live actions.
- Remote provisioning last — set
FLEET_AGENT_ALLOW_REGISTER=1only on hosts you intend to grow. Each agent re-checks local headroom beforeregister.shruns; a stale coordinator snapshot cannot override that. - Remote removal separately — set
FLEET_AGENT_ALLOW_DEREGISTER=1only 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:
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 |