Skip to content

The Flui manifest

A manifest is a written description of what you want. It sits in your repository, next to the code it describes, and you can read, review and revert it like any other file. Flui reads it, but you own it.

Once people have a file that says “this is what I want, apply it”, they want one for the next thing too, which is why the set of document kinds keeps growing.

The spec lives outside Flui

The authoritative schema lives in its own repository, flui-cloud/flui-spec, with its own versioning, narrative docs, JSON Schemas and example manifests. The TypeScript types and the validator are published to npm as @flui-cloud/spec, which Flui pulls in like any other dependency. Flui uses the schema but does not maintain it.

A manifest that named a concept internal to Flui, or that needed a Flui-specific extension, would tie you to one tool. Because the spec lives outside Flui and Flui depends on it, that cannot happen: a different tool can read the same file.

Four kinds of document

Three kinds are published in the spec:

  • Application: an application built from a source repository and deployed to a cluster. This is the one you write yourself.
  • CatalogApp: a pre-packaged building block (a database, a queue, an admin tool) installed from the catalog with the configuration the catalog publishes.
  • AccessPolicy: who may reach what. flui iam export writes one as a snapshot of every grant; flui iam apply -f reads it back.

A fourth kind comes from Flui rather than from the spec:

  • ScalingGroup: what a cluster may buy for itself and where it must stop. flui scaling export writes it, and flui scaling apply -f reads it back. It is Flui’s own document, not part of the published spec, and it carries no apiVersion. It borrows the same shape (a kind, a name, blocks underneath) because a file that reads like the others is easier to review than a new format.

One rule on that file differs from the others and catches people out: a ScalingGroup file is the whole group. A field left out is reset to its default, not left as it was. Several groups can live in one file, separated by ---. What the group is for is covered in Scaling groups.

The rest of this chapter is about the Application shape.

What an Application manifest says

An Application manifest has exactly six things at the top: kind, apiVersion, metadata, build, deploy and environments. Under deploy there are fourteen: port, exposure, healthcheck, smokeTest, resources, scaling, domain, env, volumes, files, services, browserConfig, startCommand and replaceAtStart.

Each answers one operational question:

  • Name. A stable identifier, under metadata. Every other surface (dashboard, CLI, observability) uses it, so the spec restricts it to a lowercase form that works in a hostname.
  • How to build the image. A pointer to a Dockerfile in the repository, plus the build context and the build arguments. (strategy: auto, framework detection, is deprecated and kept only so older files still validate.) The block says what to build and where, never how the build runs.
  • How to run it. The port your code listens on, the healthcheck to probe, replica limits, CPU and memory as explicit numbers (the schema also has a size preset, resources.profile, which Flui does not apply), and an optional start command.
  • How it is reached. exposure: public means a public address, with a name and a certificate. exposure: internal means no public address at all: other applications on the cluster can reach it, and you can reach it through the dashboard. exposure: none says the application does not listen at all, as with a worker or a queue consumer. Public is the default. The domain block then says whether a hostname should be assigned automatically, which one to use instead, and which certificate flow to take.
  • Configuration and secrets. A list of environment variables. An entry can carry a plain value, ask for a secret to be generated, point at an existing secret, point at a sibling application in the same project, or declare a value somebody must supply at install time.
  • Volumes. Persistent volumes the application needs, each with a name, a mount path and a size.

The schema is closed: a key it does not name is an error, not an extension. The list still grows with the spec, so treat what is written here as the current vocabulary. flui app manifest prints the field reference straight from the schema Flui has installed, and flui app manifest --example prints an annotated file to start from. Both run offline.

One file, several environments

environments is a map. You choose the keys (production, staging, preview), and each entry names the git branch it belongs to. Deploy that branch and the entry is laid over the base: environment values first, then resources, replica limits and domain.

build cannot be overridden per environment. The spec leaves it out, so what the file says about building an image is the same everywhere, and only what surrounds the image changes.

An entry that names no branch never matches anything. A validation that names no branch reports the base spec, because there is no branch to pick an entry with.

deploy.services: one building block, attached to one application

An Application can declare its own building block, most often a database, under deploy.services. Each entry names the service, the catalog block to install (block), and the environment the application needs to reach it. A value can be read off the block itself (fromService: host, port or url), or taken directly from a variable the block declares (fromBBEnv).

Deploying a manifest that declares one installs the block from the catalog, waits for it to reach a running state, and wires the resulting environment into the application before the application’s own container starts. The credential that reaches it is never a plain value on the row; it is a reference to the Secret the block generated. A deploy.services entry that names an unknown block, or a block that is not a building block, is refused before any application exists. (vaultwarden, for example, is a full application, so installing it stays a catalog install of its own.) Sizing the block through resources is accepted by the schema but refused by the deploy, which says so instead of installing the block at the catalog’s own size.

One instance belongs to one application for its whole lifecycle. Redeploying reuses it instead of provisioning a second one, and a manifest that no longer declares it stops the attachment without deleting what it already brought up. It does not give you an instance shared by two applications: nothing in the field says “the same one as that other unit’s”, so two units of a monorepo that each declare the same block end up with two separate installs of it. See How Flui reads a repository for where that shows up when the map renders a monorepo.

The composed CatalogApp surface

A CatalogApp can describe more than one workload. A composed entry (type: composed) drops the single-image body and carries a components list instead, plus a few coordination blocks with no equivalent in the Application shape:

  • components. The workloads the entry bundles. Each has its own image, environment and resources, and runs as its own app. A component can declare dependsOn (other components that must be healthy first) and when.option (include this only when a named option is on).
  • auth. Which sign-in strategies the entry supports. auth.modes lists them, and auth.default names the fallback. The modes are oidc, proxy, native and none; the one used is chosen at install time, not fixed in the file.
  • options. Install-time switches, each with a key, a label and a default. Components and post-install steps depend on them.
  • postInstall. Steps that run once the components are up, either an HTTP call or a command inside a component, each conditioned on the chosen sign-in mode or on an option.

These blocks use a few templating tokens that the consuming tool resolves at install time rather than when the file is written: a component’s own public hostname ({{self.fqdn}}) and, when OIDC is the chosen mode, the provisioned client’s {{oidc.clientId}}, {{oidc.clientSecret}} and {{oidc.issuerUrl}}. The full model is covered in Composed catalog apps.

What the manifest leaves out

  • Where it runs. A manifest does not choose a cluster, a provider or a region. Whoever runs the deploy picks the target. The same file goes to staging on one run and to production on the next.
  • Credential values. Secret values never live in the file. They are referenced by name, generated by the platform, or asked for at install time.
  • How the build runs. The file says what to build, not where or how. The build runner belongs to the tool.
  • Runtime-specific resources. The manifest describes the application, not the form it takes on whatever runs it.
  • Tool-specific extensions. Internal identifiers and vendor tuning are not allowed. Additions go through the spec, so every implementation gets them on equal terms.

Three fields that belong to the install, not the file

The same repository can be installed twice, and the two installs cannot share a name or a hostname. So three fields can be overridden when you deploy: --name, --exposure and --domain.

These are stored on the application and applied again on every later deploy, so a deploy from someone else’s machine does not revert an install to the manifest’s domain without notice. When an override replaces something the manifest says, the deploy prints both values.

Take care with --name. It is part of what identifies an application, together with the cluster, repository and branch. Passing a new one installs the same repository a second time instead of updating the first, so once you have used it, pass it on every deploy that targets that install.

The manifest owns the keys it declares

For environment variables, the file wins. A key the manifest declares is taken back by the manifest on every deploy, even if somebody had pinned a value on it in the dashboard. A key the manifest does not declare keeps whatever was set. Values passed explicitly at deploy time beat both.

There are two exceptions. A key linked to a real secret is never overwritten by a plain value from the file. A key the manifest declares but cannot resolve, because its value comes from somewhere else, keeps its stored value. Only a key the manifest stops declaring entirely is treated as a removal.

Validation is two halves

Whether a file parses and whether it would work on this installation are different questions, and getting the second one wrong can cost you eight minutes of a failed build.

The first half is the schema. It needs nothing: no cluster, no login, no network. Flui validates against the same published schema it deploys with, so the answer cannot drift from what the deploy accepts.

The second half asks this installation. It checks seven things a schema cannot answer, and, when Flui can read the repository, whether the file matches it:

  • the target cluster: does one answer to that id, and can it take a deploy;
  • the source repository: is it connected to this account;
  • the build and registry credentials: can this account build the image and push it;
  • room on the cluster: do the CPU and memory it asks for fit what is free right now;
  • where it lands: a workload cluster, or the control cluster that also runs the API, the dashboard and identity;
  • what this deploy does: create a new application, or update one that already answers to this identity;
  • how it is reached: internal, public under a name of yours, or public with no name bound yet.

Each answer is one of four: pass, warn, fail, or unknown. Unknown never blocks: an installation that could not be read has refused nothing, and reporting that as a failure would send you rewriting a manifest that was fine. Warnings describe consequences, not problems to fix. Deploying onto the control cluster is supported, for example, and is reported as a warning.

The check also returns the manifest as it would be applied, with the branch entry and the stored overrides already merged in. This is often more useful than the verdict, because it shows what the file and this installation produce together.

A manifest can satisfy the schema completely and still fail every one of the checks. When the second half cannot run (you are not signed in, or no cluster is named), the output says which half ran, so you do not assume everything was checked.

flui deploy --validate-only runs both halves and writes nothing; add --json for a machine-readable answer. flui catalog validate <file> runs the schema half only, and takes any kind. Agents have the same check as a tool.

Two lists of role names

The published spec package and the product use different names for the access roles. The spec lists viewer, editor and manager. The product accepts viewer, operator, maintainer and owner.

The mismatch goes both ways. An AccessPolicy the product accepts is rejected by flui catalog validate, which validates against the published schema. A policy written with the spec’s names is refused by flui iam apply -f. Neither tool is broken; they read two versions of the same idea.

Write the product’s names. For an AccessPolicy, run flui iam roles: it lists the roles this installation accepts, and which ones you are allowed to hand out. The role ladder is described in Who can reach what.

From manifest to running application

At deploy time Flui reads the file, lays the branch entry over it, merges the overrides stored on the install, and turns the result into whatever the target needs to host the application. The manifest is the intent, and what runs is built from it. Every deploy builds it again from the file, so the file stays an accurate description of what runs.

Where this chapter goes from here