Fleet manifest (fleet.toml)¶
shinyhub fleet reconciles a whole set of apps against a single declarative
manifest, the way kubectl apply reconciles a cluster against a directory of
YAML. You describe the apps you want, their source, visibility, and
fleet-managed config; the CLI computes the difference against what the server
actually runs and converges it.
Reconcile is client-orchestrated: the CLI fetches server state, builds the plan locally, and drives the existing per-app deploy and patch APIs. There is no server-side fleet controller and no new privileged endpoint.
Fleet commands read fleet.toml from the working directory when -f is
omitted. The previous name shinyhub-fleet.toml is still read as a fallback
when fleet.toml is absent (with a one-line deprecation note on stderr), so
existing repositories keep working; rename the file to fleet.toml at your
convenience. Pass -f <path> to point at any other location.
fleet_id = "prod-eu"
[[app]]
slug = "sales-dashboard"
source = "./apps/sales-dashboard"
visibility = "private"
[app.config]
hibernate_timeout_minutes = 30
replicas = 2
max_sessions_per_replica = 10
[[app]]
slug = "status-page"
source = "git+https://github.com/acme/[email protected]#deploy/status"
visibility = "public"
Fields¶
Top level¶
| Field | Required | Meaning |
|---|---|---|
fleet_id |
yes | Ownership scope. Must match [a-z0-9-], 1-64 chars. Stamped onto every app this manifest manages as managed_by = fleet:<fleet_id>. |
[[app]] |
yes (>=1) | One block per app the fleet should own. |
[[project]] |
no | One block per project the fleet should name. Optional: an app can declare a project without a matching block, and the project is then created unnamed. |
[[app]]¶
| Field | Required | Meaning |
|---|---|---|
slug |
yes | App slug. Unique within the manifest; a duplicate is a validation error. |
source |
yes | Where the bundle comes from (see Source resolution). |
visibility |
no | private (default), shared, or public. |
[app.config] |
no | Fleet-managed app settings (see Config). |
[app.config] - fleet-managed settings¶
| Field | Type | Meaning |
|---|---|---|
name |
string 1..128 | Friendly display name shown on the dashboard card and the detail heading. Trimmed; may not be empty. Distinct from slug, which is the URL identifier and is not settable here. |
description |
string 0..280 | One-line description shown under the name. Trimmed; "" is a real value that clears it. |
project |
string | Project slug that groups this app on the dashboard, the launchpad and the sidebar. Must match [a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?. "" is a real value that ungroups the app. The project row is created automatically if it does not exist. |
hibernate_timeout_minutes |
int | Idle minutes before hibernation. -1 resets to the server default, 0 disables hibernation, and a positive value sets the timeout. |
replicas |
int >= 1 |
Number of replica processes. See scaling. |
max_sessions_per_replica |
int >= 1 |
Per-replica admission cap for new cookieless sessions. |
autoscale |
inline table | Session-saturation autoscale policy. Reconciled atomically and drift-protected as one unit. See autoscale below. |
Only the keys you declare here are owned by the fleet manifest. An omitted key
may still be owned by the source bundle's shinyhub.toml; if neither manifest
declares it, a value set through the UI or CLI survives untouched.
Declaring name or description therefore makes the manifest their owner: a
rename in the dashboard shows up as drift on the next fleet plan and is
reverted by fleet apply. plan renders both quoted, so an empty or
space-padded value is visible rather than rendering as nothing:
[app.config] autoscale¶
Manage the per-app autoscale policy from the fleet manifest so it is reconciled
on every fleet apply and drift back to the manifest is corrected:
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | Required. Turn the policy on or off. Also gated on the global runtime.autoscale.enabled server flag. |
min_replicas / max_replicas |
int | Bounds the controller stays within. When enabled, min_replicas >= 1, max_replicas >= min_replicas, and max_replicas may not exceed the runtime max_replicas ceiling. |
target |
float (0,1] |
Target average active sessions per replica as a fraction of the per-replica cap. 0 inherits the runtime default. |
The block reconciles atomically (all four columns) and is one drift unit: fleet
plan shows a single line (e.g. autoscale off -> on(1-8 @ 0.80)) when the
server policy differs from the manifest. It is the same policy the bundle
shinyhub.toml [app] autoscale block sets; per config precedence
the fleet manifest wins. See Autoscaling for behaviour.
[[project]] - project display metadata¶
A project is created automatically the first time an app declares it, with no
name and no icon. A [[project]] block declares that metadata so a fleet
manifest can produce a fully labelled dashboard on its own:
fleet_id = "acme"
[[project]]
slug = "reporting"
name = "Quarterly Reporting"
description = "Finance-facing dashboards"
icon = "📊"
[[app]]
slug = "revenue"
source = "./apps/revenue"
[app.config]
project = "reporting"
| Field | Required | Meaning |
|---|---|---|
slug |
yes | Project slug. Unique within the manifest; a duplicate is a validation error. Same charset as an app slug. |
name |
no | Display name, up to 128 characters. Trimmed. "" is a real value that clears it. |
description |
no | One-line description, up to 280 characters. Trimmed. |
icon |
no | A single emoji shown beside the group heading. "" clears it. |
Only the keys you declare are reconciled, matching [app.config]: an omitted
key is not asserted, so a name set through the dashboard survives a manifest
that declares only an icon.
A [[project]] block that no [[app]] references is a validation error. The
fleet manifest reconciles what it declares; a project that no app joins would
be created and then never converge, since nothing in the manifest can restore
it if someone deletes it.
Projects are reconciled before apps in a single fleet apply, so an app
that creates its project lands in one that is already named.
fleet apply --prune never deletes a project. Projects are a shared namespace
that apps outside this fleet can also be in, and a project holding no apps is
a valid state an operator may be preparing. Remove one with
shinyhub projects rm <slug>.
Source resolution¶
source is resolved one of two ways:
- Local path. A relative path is resolved against the directory
containing the manifest, not the current working directory, so a manifest
is portable regardless of where
shinyhub fleetis run from. The path must exist; existence is checked in a pre-flight step before any change is made. - Git URL.
git+<url>[@ref][#subdir].@refpins a branch, tag, or commit;#subdirdeploys a subdirectory of the repository as the bundle root. The URL format is validated when the manifest is parsed; the clone happens during pre-flight.
Config precedence¶
When the same setting can come from more than one place, the fleet manifest wins:
- Fleet manifest
[app.config]- highest. A declared key is enforced on every apply; out-of-band drift is corrected back. - Bundle
shinyhub.toml[app]- durable settings are checked on every plan/apply and corrected with a config PATCH even when the bundle digest is unchanged. Boot-only settings such ascommandand startup/build timeouts continue to take effect when the bundle is deployed or started. - Server default - lowest.
If neither manifest declares a durable key, a setting managed through the UI or CLI remains outside fleet reconciliation.
Strict-mode parsing¶
Parsing reports every problem it finds, compiler-style, not just the first.
Unknown keys are rejected with a "did you mean" suggestion (a typo such as
replcias does not silently no-op). fleet_id is required and syntax-checked;
each app must have a slug and a source; duplicate slugs and invalid visibility
values are errors. A manifest with any problem is never used to make changes.
Workflow¶
1. shinyhub fleet init¶
Scaffold a manifest from the apps already deployed on the server:
Writes fleet.toml containing fleet_id and one [[app]] block per
existing app, slug-sorted, with each app's current visibility, config, and
project membership. Referenced projects are emitted once as [[project]]
blocks so grouping survives an init/plan/apply round trip.
With --source-root <dir> each source is set to <dir>/<slug> and the file
is immediately plan-able. Without it the source line is left commented so
you set each path explicitly; an unset source trips the pre-flight check with
a precise message rather than a confusing parse error.
--fleet-id is required (prompted when run interactively); the file is not
overwritten unless --force is passed.
2. shinyhub fleet plan¶
Show what apply would do, and change nothing:
plan recomputes the diff from live server state every time; it never
replays a saved plan. --detailed-exitcode makes it exit 2 when changes
are pending (useful in CI gates). --json emits a stable machine-readable
envelope; -q/--quiet collapses to the summary.
3. shinyhub fleet apply¶
Converge the fleet:
apply recomputes the same diff as plan (a prior plan is never replayed),
then for each app, in order: deploys changed apps, reconciles durable config
declared by either manifest (with [app.config] taking precedence), and stamps
ownership. Convergence is non-atomic and
continue-on-error: one failing app does not abort the rest, and the exit code
reflects the worst outcome.
| Flag | Effect |
|---|---|
--dry-run |
Identical to fleet plan; makes no changes. |
--adopt |
Take ownership of in-scope apps that exist but are not yet fleet-managed. Without it, an un-owned app in scope is reported, not modified. |
--prune |
Delete fleet-owned apps that are absent from the manifest. This also removes their persistent data directory and all bundles. |
-y/--yes |
Skip the interactive destructive-action confirmation. --prune in a non-interactive shell requires --yes. |
--retries N |
Retry attempts after the first for deploys and transient config PATCH failures. Default 1 (so two attempts total). |
--allow-unsafe-degraded-prune |
Permit prune against a server without precondition support, accepting a documented race (see Degraded mode). |
--json |
Emit the machine-readable result envelope. |
-q/--quiet |
Collapse to the summary plus result line. |
--prune is guarded: when prune candidates exist and prune will actually
run, an interactive run asks you to type the word prune to confirm.
Ownership¶
Every app a manifest manages is stamped managed_by = fleet:<fleet_id>.
This marker is what makes --prune safe: prune only ever deletes apps that
carry this fleet's marker and are absent from the manifest. An app with no
marker, or a different fleet's marker, is never pruned. The same predicate
drives the read-only dashboard surface.
Degraded mode¶
Fleet preconditions let apply patch config and prune against a precise
expected state (a compare-and-set). If the server does not advertise
precondition support, apply runs in degraded mode:
- Config patches fall back to a re-GET immediately before the write (a smaller TOCTOU window, not zero).
--pruneis disabled unless--allow-unsafe-degraded-pruneis set, which accepts the documented race that an app could change between the read and the delete.
fleet plan and apply print a warning when degraded mode is in effect.
Exit codes¶
plan and apply share one exit-code contract. apply returns the highest
applicable code.
| Code | Meaning |
|---|---|
0 |
Success, or a report was printed (including --dry-run and a clean plan). |
1 |
Usage error or manifest validation failure. |
2 |
plan --detailed-exitcode only: changes are pending. |
3 |
Transport or auth error (could not reach the server / not logged in). |
4 |
Partial: at least one app failed after retries. |
5 |
Conflicts: at least one app was skipped on a precondition 409. |
Prune candidates that are skipped because --prune was not passed do not
change the exit code; they are reported and the run is still 0.
shinyhub fleet status¶
status is the manifest-less companion to plan: it makes one read-only
GET and lists every app the server knows with its fleet ownership marker
and live deployment digest, no manifest required. It never makes changes and
returns 0 (overview printed) or 3 (transport / auth error). --json and
-q/--quiet behave as elsewhere. Use it for a quick ownership overview;
use plan when you want the diff against a specific manifest.
Dashboard surface¶
The dashboard reflects fleet ownership read-only. It is a status view, not a control surface; there is no apply, prune, or drift action in the UI.
- Ownership badge. Apps managed by a fleet show a
managed by fleet:<fleet_id>badge in the grid and on the app detail view, with a tooltip explaining the marker. - Segment filter. The apps view adds an All / Fleet-managed / Unmanaged selector (the choice is remembered across reloads) so an operator can see at a glance which apps are under fleet control.
- Live deployment digest. The app detail view shows the live content
digest of the running deployment. This is the digest of what is deployed
now, not a conformance signal: it does not by itself tell you whether the
app matches the manifest. Run
fleet planfor that. The UI labels the value accordingly so it is not mistaken for a drift indicator.