Skip to content

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:

  ~  update  reporting  name "Reporting" -> "Quarterly Revenue"

[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:

  [app.config]
  autoscale = { enabled = true, min_replicas = 1, max_replicas = 8, target = 0.8 }
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 fleet is 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]. @ref pins a branch, tag, or commit; #subdir deploys 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:

  1. Fleet manifest [app.config] - highest. A declared key is enforced on every apply; out-of-band drift is corrected back.
  2. 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 as command and startup/build timeouts continue to take effect when the bundle is deployed or started.
  3. 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:

shinyhub fleet init --fleet-id prod-eu --source-root ./apps

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:

shinyhub fleet plan

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:

shinyhub fleet apply --prune --yes

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).
  • --prune is disabled unless --allow-unsafe-degraded-prune is 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 plan for that. The UI labels the value accordingly so it is not mistaken for a drift indicator.