Skip to content

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 marked dependsOn: [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 a when is 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, healthcheck

The {{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.modes is the list of supported modes.
  • spec.auth.default is 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 postInstall exec step — the app is configured through its own CLI. Nextcloud, for instance, runs php occ user_oidc:provider Flui to 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: false

At install you pass an options map (keytrue/false); any key you omit falls back to its default. Options gate two things:

  • Components, via when.option — enabling office on 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, optional headers, body, expectStatus) against the running app.
  • exec — a command run inside a component’s container (the occ calls 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’s server_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 in envMapping, in a rendered configFile, or in a postInstall exec command, and are populated only when authMode is oidc.

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:

Terminal window
flui app list --expanded

That 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.

  • 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.