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:
--nameis part of an application’s identity. A new one installs the repository a second time.- A value passed with
--envis stored and displayed in plain text unless you name the key with--secret. --validate-onlydoes more than read the file. It also asks this installation whether the manifest would land.
flui deploy [<file>]
flui deploy # ./flui.yaml, auto-detect repo/branch/clusterflui deploy ./apps/api/flui.yaml # explicit manifest pathflui deploy --repo acme/my-app --branch mainflui deploy --env-file .env --secret DATABASE_URL --secret API_KEYflui deploy --detach # queue and exitflui deploy --validate-only # check the manifest, don't deployflui deploy --cluster my-cluster # pin a target cluster| Argument / Flag | Effect |
|---|---|
<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-tls | Publish 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-endpoint | Skip the public address and its certificate entirely. |
-e, --env KEY=VALUE | Per-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 KEY | Mark a key from --env / --env-file as sensitive. Repeatable. |
--detach | Return as soon as the build/deploy is queued. Track with flui app status. |
--no-wait | Alias of --detach. |
--no-build | Skip 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-master | Let an app that needs a real disk land on the master when the cluster has no worker. kind: CatalogApp. |
--validate-only | Check the manifest and exit, without deploying. |
--json | With --validate-only, print the result as JSON. |
--skip-checks | Bypass 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 whatevergit remote originpoints at on GitHub (any other host needs--repo), and the branch is the current one (fromgit branch --show-current), ormainif 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
--namecreates 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-stagingand 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:
flui deploy --env-file .env --secret DATABASE_URL --secret API_KEYFlui 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.
--secretonly accepts a key that this deploy actually passes. Naming a key that--envand--env-filedo 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,_KEYorPRIVATE) 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:
flui deploy --validate-onlyflui deploy --validate-only --cluster my-clusterThe 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:
flui deploy --no-build # re-deploy the current imageflui 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:
flui deploy --cert-challenge http-01 # force a dedicated per-host certificateflui deploy --cert-provider lets-encrypt-staging # untrusted test certs, no rate limitsflui deploy --hostname ip # a nip.io address instead of a domainflui deploy --no-tls # an address, no certificateflui deploy --exposure internal # no public address at allUse --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 certificateWhen 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.
flui catalog validate ./flui.yamlflui 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.