Composed catalog apps
Some software is not a single container. Nextcloud wants a
database, a cache and a background worker. WordPress wants a
database next to its web tier. Immich wants several services that
only make sense together. A composed catalog app is a single
CatalogApp manifest that bundles all of those workloads and
installs them as one atomic unit — provisioned together, torn down
together, and shown as one thing in your app list.
This chapter is about that shape: how a composed entry is put
together, how its pieces are wired, and the install-time choices
it exposes. The broader distinction between Application and
CatalogApp lives in
Catalog vs application;
this is the deep dive on the composed flavour.
What a composed app is
A composed entry sets type: composed (CatalogSpecComposed in
the schema). Instead of a single image/ports/env body like a
standalone entry, it carries a components list. Each component is
rendered into its own running app — its own workload, its own
internal address, and its own storage volume if it declares one —
exactly as if it had been installed on its own. The composed
manifest is the coordination layer on top:
it decides what gets created, in what order, and how the pieces
find each other.
The point is atomicity. You do not install a database, then a cache, then the app, then wire three sets of credentials by hand. You pick one entry, answer a few prompts, and the whole stack comes up wired and healthy — or none of it does.
The component model
Each entry under components has a name, an image, and the
same runtime blocks a standalone app would have (ports,
volumes, env, resources, scaling, healthcheck). Two extra
fields turn a flat list into a coordinated stack:
dependsOn— names of other components that must be healthy before this one starts. The installer brings components up in dependency order, so a web tier markeddependsOn: [db]waits for the database to pass its healthcheck first.when.option— a gate that includes the component only when a named install-time option is enabled. A component without awhenis always included.
Components reference each other through templating in their env.
A web component reads its database host and credentials straight
from the db component:
components: - name: db image: registry: docker.io repository: library/mariadb tag: "11" env: - name: MARIADB_USER value: wordpress - name: MARIADB_PASSWORD valueFrom: generate: secret length: 32 # ...ports, volumes, resources, scaling, healthcheck
- name: web dependsOn: - db image: registry: docker.io repository: library/wordpress tag: "6.5-apache" env: - name: WORDPRESS_DB_HOST value: "{{components.db.host}}:3306" - name: WORDPRESS_DB_PASSWORD value: "{{components.db.env.MARIADB_PASSWORD}}" secret: true # ...ports, volumes, resources, scaling, healthcheckThe {{components.db.host}} and {{components.db.env.*}} tokens
resolve at install time to the database’s in-cluster service host
and the value the database generated. The generated password is
created once on the db side and injected into web as a secret
(secret: true), so it never appears in plaintext.
Auth modes
A composed entry declares which authentication strategies it
supports under spec.auth:
spec.auth.modesis the list of supported modes.spec.auth.defaultis the one chosen when the installer is not told otherwise.
At install time you pick one mode via the install’s authMode,
which must be one of oidc, proxy, native or none. native
leaves the app’s own login in place; none disables auth wiring;
proxy puts the app behind a header-injecting proxy; oidc wires
the app to Flui’s identity provider.
OIDC auto-provisioning
When you choose oidc, Flui does the SSO plumbing for you. It
registers a web OIDC client against Flui’s own Zitadel-backed
identity provider, scoped to the install’s resolved hostname, and
records the resulting oidcAppId and oidcProjectId on the install
record. Those IDs are what let Flui clean the client up again when
you uninstall, so an SSO registration never outlives the app it was
created for.
The client’s credentials reach the app through whatever channel the
manifest declares under spec.auth.oidc:
envMapping— the issuer URL, client ID and client secret are injected as environment variables the app reads natively.configFile— a rendered config file is written into the container at a declared path.- A
postInstallexec step — the app is configured through its own CLI. Nextcloud, for instance, runsphp occ user_oidc:provider Fluito register Flui as an OIDC provider after the container is up.
Whichever channel is used, the credentials arrive through the templating tokens described below, never hard-coded in the manifest.
Install-time options
spec.options is a list of boolean feature toggles. Each option has
a key, a human label, an optional description, and a default:
options: - key: office label: "Office editing (Collabora)" description: "Adds a document-server component." default: falseAt install you pass an options map (key → true/false); any
key you omit falls back to its default. Options gate two things:
- Components, via
when.option— enablingofficeon Nextcloud pulls in the extra Collabora document-server component; leaving it off skips that component entirely. - Post-install steps, via the same
when.option— so the step that configures the optional component only runs when the option is on.
This is how one manifest can describe both a lean install and a fully-featured one without forking into two entries.
Multi-endpoint
A composed stack can expose more than one component to the outside
world. Any component with a port marked expose: true gets its own
endpoint and FQDN, resolved by Flui’s endpoint-mode resolver the
same way a standalone app’s endpoint is. Components with no exposed
port stay internal — a database or cache is reachable only from the
other components over the in-cluster service, never published.
Post-install steps
spec.postInstall is a list of actions that run after the
components are deployed and healthy. Each step is either:
http— an HTTP call (method,path, optionalheaders,body,expectStatus) against the running app.exec— a command run inside a component’s container (theocccalls above are exec steps).
Steps can be conditioned with when.authMode and when.option, so
the OIDC-provider registration only runs in oidc mode and the
office-config step only runs when office is enabled. This is where
work that can only happen against a live, configured app lives —
trusting the public hostname, registering the SSO provider, enabling
an optional feature.
Templating tokens resolved at install
Beyond the cross-component references already shown, two families of token are resolved at install time and are specific to the composed flow:
{{self.fqdn}}— a component’s own public hostname, made available before the component boots. Because the install slug is deterministic, Flui knows a component’s final FQDN up front, so a component that needs to advertise its own public URL (Collabora’sserver_name, for example) can be handed it at create time rather than reconfigured after the fact.{{oidc.clientId}},{{oidc.clientSecret}},{{oidc.issuerUrl}}— the credentials of the web OIDC client Flui provisioned for this install (see above). These appear inenvMapping, in a renderedconfigFile, or in apostInstallexec command, and are populated only whenauthModeisoidc.
How a composed install appears
In flui app list a composed install shows up as a single
bundle — kind composed, as opposed to standalone — rather than
as a loose pile of unrelated apps. The bundle reflects the whole
stack, so you reason about Nextcloud as one thing, not as
nextcloud, nextcloud-db, nextcloud-redis and a worker side by
side.
To see what is inside, pass -x/--expanded:
flui app list --expandedThat reveals the individual components, each with its own slug, status and exposure. Without the flag, the list stays at the bundle level and tells you a composed app has components you can expand.
Idempotency
Re-running an install is safe. The installer looks up the components that already exist for the install and reuses them rather than creating duplicates, so a re-entry after a partial failure or a retried job continues from where it left off instead of standing up a second copy of the stack. The same property is what makes the post-install steps and the OIDC wiring safe to reach more than once.
A note on capacity
Before a composed install starts, Flui sums the resource requests of every component that will be included and checks them against what the cluster has free, refusing the install if there is not enough room (a safety margin is kept). This is the same capacity gate that guards any install; the exact CPU-vs-memory accounting is covered in The app concept.
Where to read next
- The flui.yaml manifest — the shared envelope, including the composed-manifest surface.
- Catalog vs application — the three flavours of catalog entry and where composed sits.
- The app concept — what each component becomes once it is running, and the capacity gate in full.
flui deploy— the command-level reference for installing and deploying.