forge.ControlPlane is self-hosted: nothing of Reliant’s sits between you and the cluster. This page covers the two ways forge delivers to such a cluster, how to pick between them, and what each costs you. If you are deploying to Reliant hosting, or to a cluster you registered with forge cluster connect, delivery is handled for you and you can skip this page.
Direct apply, the default
With noapplier declared, forge env deploy renders the new release and applies it from the machine you run it on, using the kubectl context the environment’s KCL names. It never falls back to the context your shell has selected, so a stray kubectl config use-context cannot send a deploy to the wrong cluster.
Before it writes anything forge checks, live, that every Secret key and image the release references actually exists on the target, and refuses if one is missing. Then it applies migrations first, waits for each Deployment and Job to become ready, and exits non-zero if any does not. That per-resource wait is the health gate, and --rollout-timeout sets each resource’s budget (five minutes by default).
Direct apply is where every environment starts, and it is the right end state for dev and for throwaway CI clusters. For a real environment it has two limits. The machine that runs forge env deploy needs write access to the cluster, so that credential lives wherever your deploys run. And nothing keeps the cluster matching the release between deploys: forge applies when you ask, and drift is yours to notice. forge env deploy <env> <release> --live-diff runs a client-side kubectl diff against the cluster when you want to see it.
Gitless Flux delivery, opt-in
If you want a reconciler to own the cluster, declare it.applier = "flux" makes forge install Flux in the environment’s own cluster and deliver each release to it as an OCI bundle. Forge then never applies the bundle itself; a direct apply of such an environment is refused, so the declaration cannot quietly fall back.
Bootstrap, once per cluster
forge-delivery-<project>-<env>), which holds the pull-Secret copy and the identities Flux applies as. Two environments can share a cluster, because each gets its own namespace. Run it again whenever you change flux_delivery.
The pinned Flux is hardened by default: Flux’s multitenancy mode on, no cluster-admin binding for its controllers, Pod Security restricted on flux-system, and ingress to the controllers only from within flux-system. Each is overridable through forge.flux_chart() if your cluster needs otherwise.
Shipping a release
<registry>/bundle.v1/<env> pinned by digest, points the environment’s Flux at it, and waits until every Kustomization has applied it. The usual flags work: --plan, --yes, --approve <digest>, --no-wait, --timeout, --json. A release deploy cannot be scoped with --target or --frontends-only, because Flux applies the whole bundle.
The bundle registry is flux_delivery.registry when you declare it, otherwise the registry of the environment’s first cluster workload image (ghcr.io/acme/api gives ghcr.io/acme). The push uses your docker credentials, and Flux pulls with the copied pull_secret_from Secret.
Several deployers can share one environment safely. The pointer carries a fence, a count of writes so far, and every write is conditional on the pointer it read. A CI runner and an administrator, each with their own ledger, therefore share one sequence, and the one that loses a race re-reads and re-checks against what won.
When a release needs a new permission (exit 9)
The bootstrap gives Flux’s apply identities exactly the permissions the environment’s render needs, derived from the objects it contains, and records what it granted. A release that adds a kind those grants do not cover (a new CRD, a first StatefulSet or CronJob, a namespace no earlier release used) would failForbidden inside the cluster. So every deploy checks the release against the record before anything is recorded, pushed or pointed.
If your credential can grant the new permission, an admin kubeconfig on your laptop for example, the plan lists the new grants, and once you approve it the deploy re-runs the bootstrap and continues. If it cannot, such as a CI credential that may only point Flux at a bundle, the deploy stops with exit 9 and writes nothing:
--json carries the same list as delivery_grants. This is the design working as intended: a deploy credential never needs permission to write RBAC. It needs, in the delivery namespace only, create and update on the input provider, read on the grants record, and read on Flux’s resourcesets, ocirepositories and kustomizations, plus push access to the bundle registry.
Tuning
The grants follow the render. Re-running the bootstrap re-derives them from your checkout, and a grant the render no longer needs is removed by the same run.
What it refuses, and what it does not do yet
The render refusesapplier = "flux" beside a control_plane (that environment is delivered by its hub), on an environment that targets more or fewer than one cluster, and beside a forge.flux_chart() you declared yourself. flux_delivery on an environment that does not declare applier = "flux" is refused too.
Signing is capped at audit. Nothing signs a self-hosted environment’s bundles yet, so signing and flux_verify default to off here, and asking for enforce is refused when you declare it on an environment with no control plane. The release’s shape and the bootstrap report what actually takes effect. If you need signature enforcement, deliver through a control plane.
Also not available in this mode: the opt-in health gate, which is enforced by a control plane and only recorded for a self-hosted environment.
Choosing
Stay on direct apply when the cluster is yours alone, deploys run from one trusted place, and you are content to apply when you ask. Chooseapplier = "flux" when a deploy credential should not be able to write to the cluster, when several people or pipelines deploy and need one ordering, or when you want the cluster to converge to the recorded release by itself. Moving later costs one declaration and one bootstrap.
Related topics
Releases and promotion covers plans, approvals and the ledger that both modes share. What gets deployed shows the objects that land on the cluster. Agents can runforge skill load deploy/flux/self-hosted and deploy/flux for the full reference, including the Flux hardening table.