fleet-config.yaml reference
fleet launch reads a YAML file describing a whole fleet — an optional
bridge, an optional gui, and an optional map of ships — and starts
everything in one process. The file defaults to ./fleet-config.yaml; override
it with fleet launch --config-path <path>. A sibling .secrets file supplies
values referenced as secrets:VAR_NAME.
Every top-level section is optional. Only the sections present are started:
fleet launch on an empty file ({}) starts nothing and exits.
Scaffold
Section titled “Scaffold”fleet launch init writes this config and a sibling .secrets file containing
a generated credential pair for ship-a.
# fleet-config.yaml — configuration for `fleet launch`.# Every section is optional; only the sections present are started.
# The fleet-wide bridge that coordinates ships and serves the fleet API.bridge: dataDirectory: ./.fleet/bridge port: 4800 name: my-fleet-bridge # publicUrl: http://this-host:4800 # how ships reach this bridge; required if any ship is on another host # insecureNoAuth: true # DEV ONLY: serve every route unauthenticated and skip creating the first admin
# The web gui. Proxies to the bridge above by default.gui: port: 3000 # bridgeUrl: http://localhost:4800 # defaults to the local bridge
# Ships that host workspaces. Each key is the ship's default name.# Any value can use secrets:VAR_NAME to read from the sibling .secrets file.# A ship's shipToken/bridgeToken are set together or not at all.ships: ship-a: # source: local (the default) spawns the ship in this process. source: local fleetDirectory: ./fleet/ship-a port: 4700 # name: ship-a # defaults to the key above shipToken: secrets:SHIP_A_SHIP_TOKEN bridgeToken: secrets:SHIP_A_BRIDGE_TOKEN
# source: remote registers an already-running ship by URL instead of spawning it. # ship-b: # source: remote # url: http://another-host:4700 # shipToken: ${SHIP_B_SHIP_TOKEN} # the credentials that ship was already started with # bridgeToken: ${SHIP_B_BRIDGE_TOKEN} # must equal the ship's own FLEET_BRIDGE_TOKENThe generated .secrets has this shape, with independently generated values:
SHIP_A_SHIP_TOKEN=<generated secret>SHIP_A_BRIDGE_TOKEN=<generated secret>The file is written with mode 0600 and should not be committed.
Secret references
Section titled “Secret references”Every YAML value may be replaced by secrets:VAR_NAME. Fleet loads .secrets
from the config file’s directory, even when --config-path points elsewhere.
Each non-empty line is KEY=VALUE; keys match [A-Za-z_][A-Za-z0-9_]* and may
appear only once. Values are literal strings. Exact integer, true, and false
values are converted when substituted so typed config fields work:
BRIDGE_NAME=private-bridgeBRIDGE_PORT=4810SHIP_DIRECTORY=./private fleet/ship-abridge: name: secrets:BRIDGE_NAME port: secrets:BRIDGE_PORTships: ship-a: fleetDirectory: secrets:SHIP_DIRECTORYThe reference must occupy the whole value. A missing key, malformed reference, empty value, invalid name, or duplicate name fails the launch before anything starts. Secret references are resolved before schema validation, and keys themselves are never substituted.
Top level
Section titled “Top level”| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
bridge |
object | no | absent | Start a bridge in this process. |
gui |
object | no | absent | Serve the web GUI in this process. |
ships |
map of string → object | no | {} |
Ships to start and/or register. |
bridge: and gui: written with no body parse to null in YAML; both are
treated as “enabled with defaults” rather than an error. ships: with no body
is not given this treatment.
bridge
Section titled “bridge”Every field has a default, so bridge: {} is valid.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
dataDirectory |
string (non-empty) | no | ./.fleet/bridge |
Where the bridge persists ships.json, repos.json and ephemeral.json, and where its armory/ directory lives. Resolved to an absolute path. |
port |
integer | no | 4800 |
Port the bridge’s HTTP + WebSocket API listens on. |
name |
string (non-empty) | no | bridge |
Human-facing name of the bridge. |
publicUrl |
string (non-empty) | no | http://localhost:<port> |
URL ships use to reach this bridge. |
insecureNoAuth |
boolean | no | absent (authentication required) | Development only: serve every route unauthenticated, and skip creating the first admin. |
sweepIntervalMs |
integer ≥ 0 | no | 300000 (5 minutes) |
How often to check ephemeral workspaces for a closed pull request. 0 turns the sweep off, leaving POST /workspaces/sweep as the only way to run one. |
publicUrl
Section titled “publicUrl”publicUrl is handed to each ship so it can pull the
armory, so it has to resolve from the ships’ hosts,
not from the machine running the launch. The default,
http://localhost:<bridge.port>, is correct for a single-host fleet and wrong
the moment a source: remote ship is on another machine — there, localhost is
that machine.
Getting it wrong fails quietly: the ship registers, its workspaces work, and only
the armory never arrives. So fleet launch warns when a config declares one or
more source: remote ships and sets no publicUrl:
fleet launch: bridge.publicUrl is not set, so remote ships "build-box", "gpu-box" will be told this bridge is at http://localhost:4800, which on their hosts is themselves; set bridge.publicUrl to a URL those hosts can reachIt is a warning on stderr, not an error — a source: remote ship can legitimately
be on this same host behind a tunnel or a published container port, where
localhost still resolves. Local ships never trigger it.
The value is used verbatim; it is validated as a non-empty string, not parsed or
normalized like gui.bridgeUrl, so write a full URL with its scheme. The
equivalent flag on a standalone bridge is fleet bridge --public-url.
It is also what every source: local ship is pinned to (fleet ship --bridge-url), so those ships refuse an armory push from anywhere else. A value
that is not an http(s) URL cannot be a pin; rather than fail the launch, it warns
and starts the ships unpinned:
fleet launch: bridge.publicUrl "bridge:4800" is not an http(s) URL, so ships are started unpinned and will accept the first armory push they receiveShips registered with source: remote are pinned by whatever they were started
with — fleet launch does not configure a ship it did not spawn.
insecureNoAuth
Section titled “insecureNoAuth”A bridge with no users creates the first admin before it serves anything: from
FLEET_BRIDGE_ADMIN_USER, FLEET_BRIDGE_ADMIN_EMAIL and
FLEET_BRIDGE_ADMIN_PASSWORD if all three are set, otherwise by prompting on
stdin. On a headless box with none of them set there is no terminal to answer the
prompt, and the launch fails:
fleet launch: fleet-bridge has no users and stdin is not a terminal — set FLEET_BRIDGE_ADMIN_USER, FLEET_BRIDGE_ADMIN_EMAIL, FLEET_BRIDGE_ADMIN_PASSWORD to create the first admin, or start with --insecure-no-authinsecureNoAuth: true is how a fleet launch config says “this is a dev fleet”:
it skips that bootstrap and serves every route as an admin, with a banner on
stderr for as long as the bridge runs. It gives up authentication and
authorization — see authentication for exactly what.
The FLEET_INSECURE_NO_AUTH=1 environment variable is not consulted by
fleet launch; it only affects a standalone fleet bridge. Under fleet launch
the setting has to be this key.
Both fields are optional, so gui: {} is valid — as long as a bridge exists to
proxy to.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
port |
integer | no | none — Bun picks the port | Port the GUI listens on. |
bridgeUrl |
string (non-empty) | no | http://localhost:<bridge.port> |
Bridge the GUI reverse-proxies /bridge/* to. |
When bridgeUrl is omitted, the GUI targets the launched local bridge. That is
why a gui section with neither a bridge section nor a bridgeUrl is a
validation error — there would be nothing to point it at.
The value is normalized like every other Fleet URL: a bare port becomes
http://localhost:<port>, a bare host:port gains an http:// scheme, and a
full URL is used as-is.
A map, not a list. Each key names a ship and supplies the defaults for that
entry: the key becomes the ship’s name and, for local ships, its
fleetDirectory (./fleet/<key>).
Each value is one of two shapes, discriminated by source. When source is
omitted the entry is treated as local, so ship-a: {} is a complete, valid
ship.
source: local
Section titled “source: local”The ship is spawned inside the fleet launch process.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
source |
"local" |
no | local |
Discriminator. |
fleetDirectory |
string (non-empty) | no | ./fleet/<key> |
Directory holding this ship’s workspaces (<dir>/<repo>/<name>). Resolved to an absolute path. |
port |
integer | no | 4700 |
Port this ship listens on. |
name |
fleet identifier | no | the map key | Human-facing name of this ship. |
shipToken |
string (non-empty) | no | provisioned by the launch | The token this ship presents to the bridge. See ship credentials. |
bridgeToken |
string (non-empty) | no | provisioned by the launch | The token the bridge presents to this ship. |
Because port defaults to 4700 for every local ship, two or more local ships
must each set a distinct port.
source: remote
Section titled “source: remote”The ship is already running elsewhere; fleet launch only registers it with the
bridge.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
source |
"remote" |
yes | — | Discriminator. Must be written explicitly; omitting it makes the entry local. |
url |
string (non-empty) | yes | — | Base URL of the running ship. |
shipToken |
string (non-empty) | no | none | The token this ship presents to the bridge. See ship credentials. |
bridgeToken |
string (non-empty) | no | none | The token the bridge presents to this ship — the ship’s own FLEET_BRIDGE_TOKEN. |
Remote entries accept no other fields — no port, no name, no
fleetDirectory. The ship’s name is discovered by the bridge from the ship’s
own sync event, not taken from the map key.
Ship credentials
Section titled “Ship credentials”shipToken and bridgeToken are set together or not at all, on either kind
of ship. One without the other fails the launch before anything starts:
fleet launch: ship "gpu-box" sets shipToken but not bridgeToken; a ship is registered with both a shipToken and a bridgeToken, or neitherWhat each token does, and which end holds it, is covered in authentication. What matters here is the default:
- A
source: localship with neither key set is provisioned by the launch —fleet launchsettles a pair, hands it to the ship it spawns, and registers the ship with it. That is the usual case. On the first launch both halves are generated; on later launches the bridge reuses thebridgeTokenit stored for that ship and rotates only theshipToken, so a ship the roster already holds comes back with credentials the bridge can still use. See authentication for why thebridgeTokencannot be rotated out from under a running bridge. - A
source: localship with both keys set uses those instead of minting. - A
source: remoteship with neither key set is registered with no credentials, and the bridge talks to it unauthenticated. - A
source: remoteship with both keys set is registered with them. The ship itself must already have been started withFLEET_BRIDGE_TOKENset to the samebridgeToken, or the bridge’s very first call to it is rejected and the registration fails.
Environment interpolation
Section titled “Environment interpolation”For compatibility, shipToken and bridgeToken also accept ${VAR} in place
of a literal and read it from the environment:
ships: gpu-box: source: remote url: http://10.0.0.7:4700 shipToken: ${GPU_BOX_SHIP_TOKEN} bridgeToken: ${GPU_BOX_BRIDGE_TOKEN}The rules are deliberately narrow:
| Value | Result |
|---|---|
${NAME} — the whole value, optionally with surrounding whitespace |
Replaced by the environment variable NAME, trimmed. |
any string with no ${ in it |
Used literally. |
anything else containing ${ — tok-${NAME}, ${A}${B} |
Rejected. |
${NAME} where NAME is unset, empty, or only whitespace |
Rejected. |
A variable name must match [A-Za-z_][A-Za-z0-9_]*. Environment interpolation
applies only to these two fields; use .secrets references for any other value.
An unset variable is an error rather than an empty value, and this is the point of the feature: the alternative is a fleet that comes up looking healthy with a ship registered unauthenticated, which nothing would tell you about.
fleet launch: ships."gpu-box".shipToken is ${GPU_BOX_SHIP_TOKEN}, which is unset or empty in the environment; export GPU_BOX_SHIP_TOKEN, or delete the key to register this ship without credentialsA partial interpolation is rejected for the same reason — taking tok-${NAME}
literally would register a token that is not the one anybody meant:
fleet launch: ships."gpu-box".shipToken contains "${" but is not exactly one ${VAR} reference; write the whole value as ${VAR}, or as the literal secretThe resolved value is trimmed, so a variable set from a file or a here-doc with a trailing newline still works.
Validation errors
Section titled “Validation errors”Zod rejects a malformed file before anything starts; the CLI prints
fleet launch: <message> and exits 1.
| Error | Raised when |
|---|---|
config file not found: <path> |
The path given by --config-path (or ./fleet-config.yaml) does not exist. |
failed to parse config file <path> as YAML: <message> |
The file is not valid YAML. |
<path>/.secrets:<line> ... |
A .secrets line is malformed, duplicated, empty, or has an invalid name. |
<field> references secrets:<name>, which is not set in .secrets |
A config value references a key absent from the sibling .secrets file. |
<field> contains an invalid secret reference; use secrets:VAR_NAME as the whole value |
A value begins with secrets: but is not a valid whole-value reference. |
ships "<a>" and "<b>" both use port <port>; give each local ship a distinct port |
Two source: local ships share a port. Remote ships are exempt — they have no port field. |
gui is configured with no bridge to proxy to; add a bridge section or gui.bridgeUrl |
A gui section exists with neither a bridge section nor gui.bridgeUrl. |
ships."<key>".<field> is ${VAR}, which is unset or empty in the environment; … |
A shipToken/bridgeToken references a variable that is not exported, or is empty. |
ships."<key>".<field> contains "${" but is not exactly one ${VAR} reference; … |
A shipToken/bridgeToken mixes literal text with an interpolation. |
ship "<key>" sets <a> but not <b>; a ship is registered with both a shipToken and a bridgeToken, or neither |
Exactly one of the two token keys is set on a ship. |
| a zod issue list | Any schema violation: source: remote with no url; a non-integer port; a non-boolean insecureNoAuth; an empty dataDirectory, fleetDirectory, name, url, bridgeUrl, shipToken, or bridgeToken; a name that is not a valid fleet identifier; an unknown source value. |
Ordering matters when reading a failure: secret references resolve first, then schema parsing, then — per ship, in map order — environment interpolation and the both-or-neither check, then the duplicate-port check, then the gui/bridge check.
What launch actually does
Section titled “What launch actually does”- Loads
.secrets, resolves everysecrets:VAR_NAME, validates and normalizes the config, then resolves${VAR}in ship tokens. - If
bridgeis present, starts the bridge — creating the first admin unlessinsecureNoAuthis set — and keeps its manager. - Settles each ship’s credentials in map order: the configured pair if both keys
are set, otherwise a pair provisioned from the bridge for a
source: localship, and none for asource: remoteship. - Starts every
source: localship — pinned to the launched bridge’spublicUrl, and handed its pair. - Plans the registrations against the bridge’s roster: a ship the bridge already holds is skipped, and so is a config entry whose URL an earlier entry claimed.
- Registers the ships step 5 planned, at
http://localhost:<port>(local) or theirurl(remote), printingregistered ship "<key>" (<url>) with the bridgefor each. - If
guiis present, serves the GUI againstgui.bridgeUrlor the local bridge.
Non-fatal cases to expect in the log:
- With no
bridgesection, each ship logsno bridge configured; not registering ship "<key>" (<url>)— local ships still start. - A ship the bridge’s roster already holds logs
ship "<key>" (<url>) is already registered with the bridge; one held under the same name at a different URL warnsship "<key>" is already registered with the bridge at <rosterUrl>, not <url>. - Two config entries on one URL warn
ships "<firstKey>" and "<key>" both point at <url>; registering it once. - A registration that throws logs
could not register ship "<key>" (<url>): <message>and the launch continues with the next ship.
Examples
Section titled “Examples”A single machine running everything:
bridge:gui: port: 3000ships: local:That is a bridge on 4800 with data in ./.fleet/bridge, a GUI on 3000
proxying to it, and one ship named local on 4700 with workspaces under
./fleet/local.
Two local ships plus one already-running remote ship:
bridge: port: 4800 publicUrl: http://10.0.0.2:4800gui: port: 3000ships: ship-a: port: 4700 ship-b: port: 4701 fleetDirectory: /srv/fleet/ship-b builder: source: remote url: http://10.0.0.7:4700publicUrl is set here because builder is on another host: without it, that
ship would be told to pull the armory from http://localhost:4800, which on
10.0.0.7 is 10.0.0.7.
A GUI-only process pointed at a bridge on another host:
gui: port: 3000 bridgeUrl: http://bridge.internal:4800See configuring a fleet for the task-oriented
walkthrough, multi-host for spreading ships across
machines, and authentication for what the tokens and
insecureNoAuth actually control.