Skip to content

Deploy

flui deploy takes a flui.yaml manifest and rolls the application onto a cluster. The same command handles both manifest kinds:

  • kind: CatalogApp: the image already exists in a registry; Flui pulls and deploys it.
  • kind: Application: the image is built from source by GitHub Actions in the repository, then the resulting image is deployed.

You use it the same way in both cases: read the manifest, target a cluster, run. The only difference is whether a build step comes first. See the concept chapters Catalog vs Application and The build pipeline for the model.

Three things on this page often surprise people:

  • --name is part of an application’s identity. A new one installs the repository a second time.
  • A value passed with --env is stored and displayed in plain text unless you name the key with --secret.
  • --validate-only does more than read the file. It also asks this installation whether the manifest would land.

flui deploy [<file>]

Terminal window
flui deploy # ./flui.yaml, auto-detect repo/branch/cluster
flui deploy ./apps/api/flui.yaml # explicit manifest path
flui deploy --repo acme/my-app --branch main
flui deploy --env-file .env --secret DATABASE_URL --secret API_KEY
flui deploy --detach # queue and exit
flui deploy --validate-only # check the manifest, don't deploy
flui deploy --cluster my-cluster # pin a target cluster
Argument / FlagEffect
<file>Path to the manifest. Default: ./flui.yaml.
-c, --cluster <name|id>Target cluster. Auto-detected when the active profile has only one.
-r, --repo <owner/repo>GitHub repository. For kind: Application, auto-detected from a GitHub git remote origin.
-b, --branch <name>Branch to build. For kind: Application, auto-detected from the current branch, main if it cannot be read.
--name <name>Release name. Part of the application’s identity; see below.
--exposure <public|internal>Override how the app is reached. public gets an address and a certificate. internal gets no public hostname: it is reached inside the cluster and through Flui, behind sign-in.
-d, --domain <fqdn>Custom hostname for the app. Auto-assigned from the cluster’s DNS zone otherwise.
--hostname <ip|domain>Whether the address is derived from the cluster IP (a nip.io address) or sits under the cluster’s DNS zone.
--no-tlsPublish the address without a certificate for this app. The app is served over plain HTTP.
--cert-challenge <http-01|dns-01>How the certificate is validated. http-01 issues a dedicated per-host certificate and works on any reachable domain; dns-01 uses the cluster’s wildcard certificate and needs a DNS zone.
--cert-provider <lets-encrypt|lets-encrypt-staging>Which issuer signs the certificate. lets-encrypt-staging issues untrusted test certificates and avoids rate limits while you iterate.
--skip-endpointSkip the public address and its certificate entirely.
-e, --env KEY=VALUEPer-deploy variable override. Repeatable. Plain text unless marked.
--env-file <path>Read KEY=VALUE overrides from a file, one per line. # comments and blank lines are ignored. --env wins on the same key.
--secret KEYMark a key from --env / --env-file as sensitive. Repeatable.
--detachReturn as soon as the build/deploy is queued. Track with flui app status.
--no-waitAlias of --detach.
--no-buildSkip the build step and redeploy the current image. Useful when you only changed flui.yaml settings.
--image <ref>Deploy a specific image reference (e.g. ghcr.io/owner/repo:sha). Skips the build pipeline; useful for rollbacks.
--allow-masterLet an app that needs a real disk land on the master when the cluster has no worker. kind: CatalogApp.
--validate-onlyCheck the manifest and exit, without deploying.
--jsonWith --validate-only, print the result as JSON.
--skip-checksBypass the framework pre-flight checks (e.g. the Next.js standalone-output check). kind: Application only.
--api-url <url>Talk to a different API address than the active profile’s.

Auto-detect rules

flui deploy with no arguments behaves as follows:

  • The manifest is ./flui.yaml.
  • For kind: Application: the repo is whatever git remote origin points at on GitHub (any other host needs --repo), and the branch is the current one (from git branch --show-current), or main if it cannot be read.
  • The cluster is the only one in the active profile; with more than one cluster, pass -c, --cluster.

Any of these can be overridden with the corresponding flag.

If there is no flui.yaml in the current directory but there are manifests below it, the command lists them and stops. Each flui.yaml is one deployable application, so it asks you which one you meant instead of guessing.

--name identifies an install

Read this section before you use --name.

For kind: Application, Flui identifies an application by four things together: the cluster, the repository, the branch and the name. This lets one repository hold several applications (a monorepo with one flui.yaml per deployable), and lets you install the same repository twice, for example once for production and once for a demo.

--name sets the fourth part. It identifies the install itself rather than labelling an existing one. This has two consequences:

  • A new --name creates a new application. It does not rename the old one. The first install stays where it is, still running and still costing the same.
  • You must pass it every time. Flui looks the application up by name before it can read anything stored about it. If you deploy once with --name api-staging and then deploy again without it, the lookup misses, and a second application is created from the same repository and branch.

When you pass --name, the deploy prints it in the header before anything is submitted. If it is not the one you expect, stop there.

--exposure, --domain and the other address flags (--hostname, --no-tls, --cert-challenge, --cert-provider, --skip-endpoint) are install-time overrides too. Unlike --name, they are stored on the application and re-applied on later deploys. That stops somebody else’s flui deploy from quietly reverting your install to whatever the manifest says, and it also means a one-off --no-tls stays until you change it. Only --name has to be repeated.

On a kind: CatalogApp install, --name is only the display name, and the warning above does not apply.

What you type on the command line stays plain text

--env overrides values but does not protect them. Every value you pass with --env or --env-file is stored as an ordinary setting: it is kept as text and shown to anyone who can read the application’s configuration. A password passed this way stays a plain value for as long as the application exists.

Use --secret to mark a value as sensitive:

Terminal window
flui deploy --env-file .env --secret DATABASE_URL --secret API_KEY

Flui hands a key named with --secret to the application as a secret instead of plain configuration, and stops showing it: flui app env list tells you the key is set but never what it holds.

  • --secret only accepts a key that this deploy actually passes. Naming a key that --env and --env-file do not declare is an error, because a typo there would otherwise deploy the credential in plain text.
  • The deploy prints a warning when a key looks like a credential (its name contains SECRET, PASSWORD, TOKEN, _KEY or PRIVATE) and is not marked. The warning does not stop the deploy.

--secret is carried on kind: Application deploys. A kind: CatalogApp install does not send it, so a value passed there with --env stays a plain value.

Prefer --env-file for anything sensitive

A value written into the command itself goes into your shell history, and is visible to every other process on the machine while the command runs. --env-file keeps the value out of the command: the CLI reads the file, and the value never appears in the arguments.

For a single value on an application that already exists, use flui app env set <app> <KEY> instead. It has no --value flag: it prompts, or reads standard input, so the value never appears in the command.

The manifest is the other place a variable can be declared, and usually the better one. See Environment variables.

--validate-only asks two questions

The first is whether the file parses and matches the spec. It is answered with no cluster, no login and no network, which is why it is fast.

A file can be well formed and still have nowhere to go, so when there is an installation to ask, Flui asks it as well:

Terminal window
flui deploy --validate-only
flui deploy --validate-only --cluster my-cluster

The second half reports, in plain rows:

  • Target cluster: does it exist, is it ready.
  • Source repository: is that repository connected to Flui.
  • Build and registry: are the credentials the build needs in place.
  • Room on the cluster: measured against the CPU and memory this manifest asks for.
  • Where it lands: whether this deploys onto a workload cluster, or onto the control cluster alongside the API and the dashboard.
  • What this deploy does: whether it creates a new application or updates an existing one.
  • How it is reached: the address, and whether the pieces behind it are there.

When the repository is connected, more rows follow on what Flui read in it: the Dockerfile and build context, the port, the health path, the variables, the services it needs, and whether the manifest is current.

A row can also come back as unknown, which means this installation could not answer that question. An unknown row never blocks a deploy. A lookup that failed says nothing about the manifest, and treating it as a refusal would send you to rewrite a manifest that was correct.

The second half is skipped, with the reason printed, when you are not signed in, when no cluster can be resolved, or when the installation cannot be reached. The output always says which halves ran, so a skipped check is never mistaken for a passed one.

The installation half has three limits: it runs for kind: Application manifests, it needs the schema check to pass first, and it checks the manifest as written. The install-time overrides above are not part of it, and the branch is --branch or main.

--json prints the whole answer (validity, kind, errors, warnings and the installation half) as one JSON object, and exits non-zero only when the manifest is invalid; whether it would deploy is in the object. Without --json the command also exits non-zero when the checks say it would not deploy, so you can use it as a gate in a script.

--api-url points the command at a different API address than the one in the active profile. It is for testing against an installation you run yourself; leave it alone otherwise.

Fast iteration

When you change only configuration in flui.yaml (variables, ports, healthcheck, endpoint) and don’t need a new image:

Terminal window
flui deploy --no-build # re-deploy the current image
flui deploy --image registry/owner/repo:abc123 # deploy a known-good image

--no-build skips the build entirely. --image pins a specific image you already trust, for a rollback or a hand-picked tag.

How the app is reached

By default, Flui reads the cluster setup and gives the app an address with a certificate:

  • with a DNS zone and a wildcard certificate, the app reuses the shared wildcard (dns-01, no per-app validation);
  • otherwise it issues a dedicated per-host certificate validated over HTTP (http-01).

Override that when you need to:

Terminal window
flui deploy --cert-challenge http-01 # force a dedicated per-host certificate
flui deploy --cert-provider lets-encrypt-staging # untrusted test certs, no rate limits
flui deploy --hostname ip # a nip.io address instead of a domain
flui deploy --no-tls # an address, no certificate
flui deploy --exposure internal # no public address at all

Use --cert-challenge http-01 when you want an app to hold its own certificate even though the cluster has a wildcard available. --no-tls keeps you clear of certificate rate limits while you iterate. The app is then served over plain HTTP, so do not leave it that way. --skip-endpoint skips the address and the certificate altogether; you can set them later from the app.

--exposure internal works differently from all of these: the app gets no public address, other applications on the cluster can reach it, and you reach it through the dashboard.

These flags work on both manifest kinds. On a kind: Application deploy the same settings also live in your flui.yaml under domain: (hostnameMode, certChallenge, certificateProvider):

domain:
auto: true
tls: true
certChallenge: http-01 # dedicated per-host certificate

When a flag contradicts the manifest, the flag wins, and the deploy header shows the value it is using.

Single-node clusters and --allow-master

An application that asks for a real disk of its own is placed on a worker, not on the master. On a cluster with no worker at all, that deploy fails instead of falling back silently.

--allow-master lets such an app land on the master anyway. It is meant for small setups: a single-node cluster, or a choice to keep one app on the control plane. Without the flag, the safer default applies.

The flag is carried on a kind: CatalogApp install. See Storage classes and dedicated placement.

Deploys made by an agent

A flui deploy you type yourself never pauses. A deploy requested by an agent does: the call stops and becomes a question for a person to answer. Checking a manifest does not ask, because it changes nothing. See The action cycle.

flui catalog validate <file>...

Validates one or more flui.yaml manifests against the Flui spec. It is read-only and entirely local: nothing is deployed, nothing on a cluster is touched, and no login is needed.

Terminal window
flui catalog validate ./flui.yaml
flui catalog validate ./catalog/vaultwarden.flui.yaml ./catalog/memos.flui.yaml

<file>... is repeatable, so you can pass several manifests at once. The validator covers both kind: CatalogApp (full schema check) and kind: Application (lighter checks, because some fields are filled in by the build pipeline and are not required at validation time). It exits non-zero if any file fails.

This is the file half on its own, for several files at once. Use flui deploy --validate-only when you want the installation half too.