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 exportwrites one as a snapshot of every grant;flui iam apply -freads 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 exportwrites it, andflui scaling apply -freads it back. It is Flui’s own document, not part of the published spec, and it carries noapiVersion. It borrows the same shape (akind, 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: publicmeans a public address, with a name and a certificate.exposure: internalmeans no public address at all: other applications on the cluster can reach it, and you can reach it through the dashboard.exposure: nonesays the application does not listen at all, as with a worker or a queue consumer. Public is the default. Thedomainblock 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 declaredependsOn(other components that must be healthy first) andwhen.option(include this only when a named option is on).auth. Which sign-in strategies the entry supports.auth.modeslists them, andauth.defaultnames the fallback. The modes areoidc,proxy,nativeandnone; the one used is chosen at install time, not fixed in the file.options. Install-time switches, each with akey, alabeland adefault. 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
flui-cloud/flui-spec: the authoritative spec, with field-by-field schema, narrative docs, JSON Schemas and examples.- The app concept: what the manifest produces.
- Catalog vs application: the other kind and how it differs.
- Composed catalog apps:
the multi-component
CatalogAppshape in depth. - Scaling groups: what the fourth document kind is for.
- Who can reach what:
the roles an
AccessPolicynames. - The build pipeline: what happens between a manifest and a running container.
- DNS as part of the deploy contract and TLS and certificates: what the domain block triggers.