The fleet.yaml file¶
A fleet file is the fleet.yaml that names the machines you run — and how
to reach each one's spinloop daemon —
so spinloop fleet can observe and drive them all from one
place, and spinloop harness open
can pick one of them for you. Like a Spinloop, it is a small
declarative file meant to be committed: it names the fleet, and it holds no
secrets — a token is named by the environment variable that holds it, never
written in the file.
# fleet.yaml — the machines, and how to reach each
prefer: idle # optional; idle (default) or active
wake: on # optional; on (default) or off
apiKeyEnv: REMOTE_KEY # optional; the default engine key for kind: remote nodes
nodes:
- name: studio # required, unique; what you type at `fleet start <node>`
host: studio.local # required for a daemon node; a LAN name, tailscale name, or address
port: 4242 # optional; the daemon's control API port (4242 when omitted)
tokenEnv: STUDIO_TOKEN # optional; the *name* of the variable holding the daemon token
engineTokenEnv: STUDIO_ENGINE_KEY # optional; ditto, for the key its engine is gated with
file: ./studio/Spinloop # optional; what `fleet start`/`deploy` reads for this node
engine: # optional; where its *engine* answers, when the daemon cannot say
host: https://engine.example
port: 18080
path: /v1
- name: qwen # for a kind: remote node, the registered environment's name
kind: remote
instance-type: g6e.2xlarge # optional; the EC2 type its environment launches as
gateway: # optional; where a spinloop gateway serves this fleet
url: http://gateway.internal:4000 # required in the section, with a scheme
tokenEnv: GATEWAY_TOKEN # optional; OPENAI_API_KEY when absent
name: remote-llms # optional; labels the gateway in a model picker
The file is found the way a Spinloop is: ./fleet.yaml in the working
directory, or --fleet <path> — short -f on every spinloop fleet
subcommand except fleet logs, where -f is the follow flag and the fleet
file takes the long form only. A missing file fails, naming the expected path
and how to create one.
Fields¶
Top level¶
| Field | Required? | Meaning |
|---|---|---|
nodes |
yes | The node entries — at least one |
prefer |
no | idle (default) or active — how routing ranks several nodes that could all serve; see Spreading or consolidating |
wake |
no | on (default) or off — whether routing may start an engine on a node that is not running one; see Waking |
concurrency |
no | The most work the fleet may have in flight at once, for the spinloop orchestrator; see Concurrency |
apiKeyEnv |
no | The variable holding the key the fleet's kind: remote nodes share; see Tokens |
gateway |
no | The address a spinloop gateway serves the fleet under; see Gateway |
A node¶
| Field | Required? | Meaning |
|---|---|---|
name |
yes | Unique in the file; what you type at fleet start <node>. For a kind: remote node, the registered environment it drives |
host |
for kind: daemon |
Where the daemon answers — a LAN name, a tailscale name, or an address |
port |
no | The daemon's control API port; 4242 when omitted |
kind |
no | daemon (default) or remote; see Remote environments |
tokenEnv |
no | The variable holding this daemon's bearer token; none means no authentication (a loopback-only daemon) |
engineTokenEnv |
no | The variable holding the key this node's engine is gated with; see Tokens |
engine |
no | An override of where the engine serves — host, port, path, each optional; see Where a node's engine answers |
file |
no | The Spinloop that describes what this node runs; see A node's Spinloop source |
instance-type |
no, kind: remote only |
The EC2 instance type the environment launches as, e.g. g6e.xlarge; see Remote environments |
tags |
no | Key/value pairs naming the kind of work the node takes on; only the spinloop orchestrator reads them; see Tags |
The gateway section¶
| Field | Required? | Meaning |
|---|---|---|
url |
yes | The gateway's address, carrying a scheme — http:// or https:// |
tokenEnv |
no | The variable holding the gateway's token; OPENAI_API_KEY when absent |
name |
no | The label the gateway wears in a model picker; the address's host when absent |
Where a node's engine answers¶
A node's host and port name its daemon, which is a different port from
the engine it supervises. For routing
spinloop needs the engine's, and the daemon reports it — so most nodes need
nothing more. Declare an engine block for the cases a daemon cannot describe:
nodes:
- name: containerised
host: docker-host
engine:
port: 18080 # published port, not the one it binds inside
- name: proxied
host: node.local
engine:
host: https://engine.example # a reverse proxy in front of the engine
path: /openai # when it is not the usual /v1
Each field falls back independently to what spinloop would otherwise derive: the
node's own host, and the port and path the daemon reports.
An engine bound to loopback answers only on its own machine. Routing to it from
elsewhere fails with that explanation rather than a bare connection refused —
bind the engine to a reachable address (llama.cpp's --host 0.0.0.0), or
declare an engine block, which is you taking responsibility for reachability.
Remote environments¶
kind (defaulted to daemon) says how the fleet reaches a node. A node can
also be an spinloop remote environment rather than a
machine: its name is the registered environment it drives — no host needed
— and it is reached through its control plane, which signs each call with your
AWS credentials, so it needs no bearer token:
nodes:
- name: qwen # the registered environment, and what you type at `fleet start <node>`
kind: remote
The environment's control URLs live in its remote.json (under
~/.config/spinloop/remotes/<name>/), written by spinloop remote deploy — or
by spinloop fleet deploy, which
creates it from the fleet file itself — and never stored in the fleet file. So a
daemon and an environment sit side by side as the same kind of row, and an
environment that has not been deployed yet shows as config-error on its row
rather than blanking the fleet. See
examples/fleet-remote and
examples/fleet-mixed.
A kind: remote node may also name the EC2 instance type its environment
launches as, with instance-type (a family and size separated by a dot, e.g.
g6e.xlarge):
It is a property of the cloud environment, not of the fleet's view of it:
fleet deploy records it on the environment, and the environment's next
fresh launch uses it. A re-wake of a stopped instance keeps the type it was
launched with — EC2 cannot resize a running or stopped box — so a changed value
takes effect only after the instance is terminated (an idle sweep or
spinloop remote stop) and launched again. Omitted, the environment launches as
its control plane's default type. Naming instance-type on a kind: daemon
node is a configuration error: a daemon's hardware is the operator's to choose,
not the fleet file's.
A node's Spinloop source¶
Both fleet deploy (for a kind: remote node's environment) and fleet start
(for a kind: daemon node's engine) need to know what Spinloop file describes
what a node runs. A node names it with file, resolved relative to the fleet
file:
file is optional, because the node's own name already doubles as a lookup
key. When it is absent, resolution tries, in order:
nameregistered as aspinloop alias(spinloop alias add qwen ./envs/qwen.Spinloop) — the same lookupspinloop remote deployperforms for a Spinloop argument;- a subdirectory named after the node, beside the fleet file —
qwen/Spinloopnext tofleet.yamlfor a node namedqwen, no fields needed on either side.
A fleet laid out as one subdirectory per node therefore needs nothing beyond
each node's own name:
Nothing resolving is a per-node error naming all three ways a source could have
been given. For fleet deploy that always fails the node (there is nothing to
create an environment from); for fleet start on a kind: daemon node it
likewise fails that node's start — there is no fallback to a plain, config-less
start once this field exists. A kind: remote node's start is unaffected by
any of this: what it serves is fixed at deploy time, not pushed at start time.
This does not apply to spinloop dashboard's s key, which still starts
the selected node with a plain start, whatever the CLI's fleet start would
resolve for it.
Spreading or consolidating¶
prefer decides which node wins when several could all serve you:
idle(the default) — the machine quiet longest wins. A node that is mid-request is the least idle of all, so it is the last one chosen. Use it when several people share the fleet, or you run several agents at once.active— the most recently active wins, consolidating sessions onto one engine and leaving the others free to be woken for another model, or left asleep.
spinloop harness open --prefer <value> and spinloop fleet route --prefer <value>
override the file for one command, which is the cheap way to see what the other
setting would do before committing to it.
Waking¶
wake decides whether routing may start an engine on a node that is not
running one:
on(the default, and the behaviour of a file that declares nothing) — when nothing is serving, spinloop starts a node and waits for its engine to answer before the agent launches or the request is answered.off— a request nothing is serving fails rather than starting anything, naming the node that would have been woken and thespinloop fleet start <node>command that would start it. Use it where the machines are not to be started on demand — the models are loaded by hand, or someone else drives the starts.
An explicit --no-wake still refuses to start anything, whatever the file
says; an explicit spinloop fleet start does the opposite — it always starts,
because it was asked.
A node MAY declare its own wake, overriding the file's setting for that
node alone:
wake: on
nodes:
- name: gpu-box
host: 198.51.100.7
- name: prod
kind: remote
wake: off # this one node stays asleep even though the fleet wakes
This matters most for a kind: remote node, whose wake boots a billed cloud
instance rather than starting a process on a machine you already run — so you
can leave the fleet's daemons on wake: on while deciding a given remote
environment's waking separately, in either direction: wake: off on one node
under a fleet that otherwise wakes, or wake: on on one node under a fleet
that otherwise does not.
Tags¶
A node's tags name the kind of work the node takes on — key/value pairs the
operator chooses:
They are how an spinloop orchestrator item chooses its
node: an item names the tags of the nodes it may run on, and it matches a node
only where every one it names is a tag the node carries. A node with no tags
takes only items that name none. Routing, waking and the dashboard do not read
them — tags belong to the orchestrator's matching alone.
Concurrency¶
concurrency is the pace this fleet works at, for the
spinloop orchestrator: how much work it may take at once.
The limits are a ceiling the operator sets, not a measurement of the engines'
load:
concurrency:
total: 8 # the most items the fleet may have in flight at once
tags:
"gpu=a100": 4 # and, per tag, the most in flight on nodes carrying it
An admitted item counts against total and against the limit of every tag it
names, and it frees its counts when it ends. A limit on a tag no node carries
is a configuration error naming the tag, as is a limit that is not a positive
integer. A file that declares no concurrency has no limit: the orchestrator
admits as fast as the nodes match.
Gateway¶
gateway names the address this fleet is served under by a
spinloop gateway: a launch routed through this file is
pointed at the gateway rather than at a node. The gateway has done the choosing,
so the launch queries no node and wakes none:
nodes: …
gateway:
url: http://gateway.internal:4000 # required, with a scheme
tokenEnv: GATEWAY_TOKEN # optional; OPENAI_API_KEY when absent
name: remote-llms # optional; labels the gateway (see below)
A launch through such a file is pointed at the section's address — the agent's
base URL, with the OpenAI-compatible /v1 prefix added when it carries no path
— and the token is resolved from the variable the section names —
OPENAI_API_KEY when it names none — the way a key is resolved elsewhere: an
ENV instruction, then the process environment, then the .env beside the
Spinloop. A variable set nowhere fails the launch before anything is written,
naming the variable.
As with a node's choice, the launch reports the address on stderr before the
agent starts.
This is how a machine that holds the fleet file points a harness at the fleet:
A launch reads the section when it is there, so a Spinloop
beside the file needs only the model, and the address travels with the file.
spinloop fleet route answers a file that names a gateway the same way — the
address, and that no node is queried and nothing is started.
When a launch through the gateway has no model of its own to route by (see
Launching the harness), it needs a
way to label the provider it configures — otherwise every gateway a fleet might
name would collide under the same generic id. name supplies that label
directly; with none given, the section's address's host stands in (e.g.
localhost:4000). Either way opencode and Pi show it the way a remote
environment is shown — Gateway (remote-llms) rather than a bare
OpenAI-compatible, the same pattern as llama.cpp (dev-2).
Tokens¶
The file names secrets by the variables that hold them; the values are resolved
from the process environment first, then a .env beside the fleet.yaml — the
same precedence spinloop uses everywhere, so an exported value wins and the
.env only fills a gap. Put the secrets there:
There are three references, all resolved the same way:
tokenEnv— the node's daemon bearer token. A node with notokenEnvis contacted without authentication, which is correct for a daemon bound to loopback. Any node reachable over the network needs a token — the daemon refuses to listen on a non-loopback address without one.engineTokenEnv— the key the node's engine is gated with. The two are different credentials — one authorises driving the node, the other authorises using its engine — and a node may need either, both, or neither. The daemon never hands its engine's key out: it says only that one is required.apiKeyEnv(top level) — the default key for everykind: remotenode. A remote environment is always keyed, so a node that names noengineTokenEnvof its own takes the fleet-wide reference. A node's ownengineTokenEnvoverrides it, so one remote may carry a distinct key while the rest of the fleet shares one. Akind: daemonnode never takes the fleet-wide reference: it is gated only by its ownengineTokenEnv.
- name: gated
host: gated.local
tokenEnv: GATED_TOKEN # to drive the daemon
engineTokenEnv: GATED_ENGINE_KEY # to talk to its engine
A kind: remote environment is always keyed, so it needs an engine key too —
its engineTokenEnv works as above, and a fleet-wide apiKeyEnv is the
default for every remote node that does not name one of its own:
apiKeyEnv: REMOTE_ENGINE_KEY # the default for every kind: remote node
nodes:
- name: qwen
kind: remote
engineTokenEnv: OTHER_KEY # overrides it for this node
A reference naming a variable that is set nowhere is reported against that
node, naming the variable, in the same way a missing daemon token is — on the
row as config-error, so a typo shows up there rather than as a mysterious
unauthorized, and a launch fails before it starts the agent rather than
pointing it at a gate it cannot pass.
What the file refuses¶
A fleet file is checked when it is read, and a file that fails is refused with the reason named:
- no
nodes— list at least one; - a node with no
name, or two nodes with the same name; - a
kind: daemonnode with nohost; - an unknown
kind— onlydaemonandremoteare supported; - a
kind: remotenode whosenameis not shaped like a registered environment name (no/, no trailing.json) — the name is the key of the environment it drives; - an
instance-typethat is not shaped like an EC2 instance type (a family and size separated by a dot, e.g.g6e.xlarge), andinstance-typeon akind: daemonnode at all; - a
preferother thanidleoractive, naming both accepted values; - a
wakeother thanonoroff, naming both accepted values; - a
gatewaysection with nourl, and aurlwith no scheme.
Token references are not checked at read time — a tokenEnv that resolves to
nothing shows up per node when a command needs it, as described under
Tokens.
Examples¶
Fleet files, each with a walkthrough:
examples/fleet-local/— a fleet of one, on your own machineexamples/fleet/— a small LAN fleet, all defaultsexamples/fleet-docker/— a runnable multi-node fleet in containersexamples/fleet-remote/— a fleet of cloud environmentsexamples/fleet-mixed/— daemons and environments side by sideexamples/gateway-docker/— a fleet behind its gateway
See also¶
spinloop fleet— the commands that read this filespinloop gateway— the server agatewaysection names- The
Spinloopfile — the format a node'sfilepoints at spinloop daemon— what runs on each node