Deploy Fly Machines Offline
Show the whole prompt
Read https://intentius.io/chant/tutorials/local-fly/.
In `examples/local-fly`, run `npm install` and `npx chant run fly --no-record`. The run boots mudflaps and builds, applies and verifies the app there, then tears it all down.
Run it two more times and show me the output of each run.
Keep the local endpoint in `ops/fly.op.ts` and set no `FLY_API_TOKEN`.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/tutorials/local-fly/.In `examples/local-fly`, run `npm install` and `npx chant run fly --no-record`. The run boots mudflaps and builds, applies and verifies the app there, then tears it all down.Run it two more times and show me the output of each run.Keep the local endpoint in `ops/fly.op.ts` and set no `FLY_API_TOKEN`.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.The local-fly
example is the smallest complete Fly deploy: one App and one Machine,
authored declaratively and applied straight to the Machines API. It runs against a
local emulator with no Fly account, credentials or cost, and deploys to a real
Fly org by dropping a single override.
| Resource | Serializes to | Applied by | Target |
|---|---|---|---|
| App + Machine | flaps create bodies (JSON) | flyApply (direct Machines API) | mudflaps (:4280) |
Fly’s control plane is the Machines API (“flaps”). There is no server-side
declarative apply and no state file, so flyApply does the reconcile
itself: GET-then-create per resource, then poll /wait until each machine
reaches started. It calls the flaps REST API directly, without flyctl or
a shell-out. The local target is mudflaps, a stateful fake of the same API, so
the applier runs against a real flaps-shaped control plane offline.
The infra
Section titled “The infra”src/infra.ts is one App and one Machine, authored as typed resources:
import { App, Machine, MachineConfig, MachineGuest, Fly } from "@intentius/chant-lexicon-fly";
// org_slug is required by the Machines API. Fly.OrgSlug resolves from FLY_ORG at// build time, default "personal" offline.const app = new App({ name: "local-fly-demo", org_slug: Fly.OrgSlug });
const web = new Machine({ name: "web", region: "iad", config: new MachineConfig({ image: "flyio/hellofly:latest", guest: new MachineGuest({ cpu_kind: "shared", cpus: 1, memory_mb: 256 }), }),});
export { app, web };build:fly serializes these into the create bodies flyApply POSTs: the App
into POST /v1/apps { app_name, org_slug }, the Machine into
POST /v1/apps/local-fly-demo/machines { name, region, config }. The serializer
stamps managed-by: chant into config.metadata on its own, as the ownership
marker the owned-only prune reads back.
The op
Section titled “The op”ops/fly.op.ts is one line, the flyDeploy composite:
import { flyDeploy } from "@intentius/chant-lexicon-fly";
export default flyDeploy({ app: "local-fly-demo" });flyDeploy lays out Emulator → Build → Apply → Verify → Teardown and wires
each phase to a fly activity. The Apply step’s flaps endpoint defaults to local
mudflaps (http://localhost:4280).
Prerequisites
Section titled “Prerequisites”- Node.js and
npm. - Docker. The
Emulatorphase (flapsUp) boots the mudflaps container; it is the only step that shells out, which is why Docker is the sole prerequisite.
Run it
Section titled “Run it”cd examples/local-flynpm install
chant run flyThe Op runs in-process through five modeled phases:
[phase] Emulator ✓ flapsUp boots ghcr.io/intentius/mudflaps on :4280[phase] Build ✓ chantBuild(script=build:fly) writes dist/fly.json[phase] Apply ✓ flyApply(planPath=dist/fly.json) created app + machine, waited to started[phase] Verify ✓ httpCheck(.../machines) machine reached started[phase] Teardown ✓ flapsDown removes the mudflaps containerEvery phase is a modeled activity, not a shell script. Add --json for
machine-readable output.
What the reconcile proves
Section titled “What the reconcile proves”With flyApply, the platform holds the state instead of a local file.
Three re-runs show it:
- Re-apply is a no-op. A second
chant run flyfinds the machine unchanged and skips it, sinceflyApplycompares the desiredconfigagainst the live machine. - An image change updates in place. Edit
config.imageinsrc/infra.tsand re-run;flyApplyleases the machine, updates it, and waits the newinstance_idtostarted. - Removing the machine prunes it. Delete the
webmachine and re-run with prune on (flyDeploy({ app: "local-fly-demo", prune: true })), and the owned-only prune destroys it, because it carries themanaged-by: chantmarker. A machine created directly in mudflaps, without that marker, is left untouched.
Run it against real Fly
Section titled “Run it against real Fly”The same op deploys to a real Fly org with one change: drop the local endpoint override and give it a token:
export default flyDeploy({ app: "local-fly-demo", endpoint: null });export FLY_API_TOKEN=... # a Fly deploy tokenchant run flyendpoint: null drops the local override, so flyApply falls through to
FLY_FLAPS_BASE_URL (or Fly’s default https://api.machines.dev) and sends
Authorization: Bearer $FLY_API_TOKEN. Only the endpoint and the token differ:
mudflaps accepts unauthenticated calls, and real Fly needs the token. Real mode boots
no container (no Docker required); the Apply step’s own /wait on started is
the verification there.