Skip to content

Composed catalog apps

Some software is more than a single container. Nextcloud needs a database and a cache, plus an optional document server. WordPress needs a database next to its web tier. Immich needs 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 unit. They are provisioned together, removed together, and shown as one entry in your app list.

This chapter covers how a composed entry is put together, how its pieces are wired, and the choices it offers at install time. The broader difference between Application and CatalogApp is in Catalog vs application.

What a composed app is

A composed entry sets type: composed (CatalogSpecComposed in the schema). Instead of the single image, ports and env body of a standalone entry, it carries a components list. Each component becomes its own running app, with its own workload, its own internal address and its own storage volume if it declares one, as if it had been installed on its own. The composed manifest coordinates them: it decides what gets created, in what order, and how the pieces find each other.

You do not install a database, then a cache, then the app, and then wire three sets of credentials by hand. You pick one entry, answer a few prompts, and the whole stack comes up wired together. If a required component fails, the install stops and is marked failed. Running it again picks up where it stopped (see Idempotency below).

The component model

Each entry under components has a name, an image, and the same runtime blocks a standalone app has (ports, volumes, env, resources, scaling, healthcheck). Two extra fields coordinate the 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: 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. Here a web component reads its database host and credentials 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 internal address 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 sets up single sign-on for you. It registers a web OIDC client with 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. Flui uses those IDs to remove the client when you uninstall, so an SSO registration never outlives its app.

The client’s credentials reach the app through the 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.

In every case the credentials arrive through the templating tokens described below and are 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 that sets each key to true or false. Any key you omit falls back to its default. Options control two things:

  • Components, via when.option. Enabling office on Nextcloud adds the Collabora document-server component; leaving it off skips that component.
  • Post-install steps, via the same when.option, so the step that configures the optional component runs only when the option is on.

This way one manifest describes both a minimal install and a full one, without splitting into two entries.

Multi-endpoint

A composed stack can expose more than one component to the internet. Any component with a port marked expose: true gets its own endpoint and FQDN, resolved by Flui’s endpoint-mode resolver in the same way as a standalone app’s endpoint. Components with no exposed port stay internal: a database or cache is reachable only from the other components over the internal address, and is 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 runs only in oidc mode and the office configuration step runs only when office is enabled. Use these steps for work that needs a live, configured app, such as trusting the public hostname, registering the SSO provider or enabling an optional feature.

Templating tokens resolved at install

Besides the cross-component references shown above, three groups of tokens are resolved at install time and are specific to composed installs:

  • {{self.fqdn}}: a component’s own public hostname, available before the component boots. The install slug is deterministic, so Flui knows a component’s final FQDN in advance. A component that needs to advertise its own public URL (Collabora’s server_name, for example) gets it at create time instead of being reconfigured later.
  • {{oidc.clientId}}, {{oidc.clientSecret}}, {{oidc.issuerUrl}}: the credentials of the web OIDC client Flui provisioned for this install (see above). They appear in envMapping, in a rendered configFile, or in a postInstall exec command, and are filled in only when authMode is oidc.
  • {{install.resolvedFqdn}}, {{components.<name>.fqdn}}: the install’s public hostname and a component’s, available to postInstall steps once the endpoints exist.

How a composed install appears

In flui app list a composed install shows up as a single bundle of kind composed (a single app is standalone), instead of as several unrelated apps. The bundle represents the whole stack, so Nextcloud appears as one entry, not as an app, a database and a cache 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

You can safely run an install again. The installer looks up the components that already exist for the install and reuses them instead of creating duplicates. After a partial failure or a retried job, the install continues where it left off instead of creating a second copy of the stack. For the same reason, the post-install steps and the OIDC wiring are 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. If there is not enough room (keeping a safety margin), it refuses the install. This is the same capacity check that applies to any install. How CPU and memory are counted is covered in The app concept.