Skip to content

Fly Deploy with Checkpoint Rollback

The fly-deploy-rollback example is for teams already on Fly Machines. It wraps a transactional rollback boundary around a Fly deploy, using a Sprite checkpoint as the commit point:

  • declarative infra-as-code for your Machines — a Fly App and a Machine you author in TypeScript, applied straight to the Machines API;
  • a Sprite sandbox (Fly’s stateful, checkpointable VM) whose checkpoint is a clean rewind point;
  • checkpoint-as-compensation — when a risky post-deploy step fails, restoring the checkpoint rewinds the whole sandbox to its known-good state in roughly the time a VM restore takes, instead of a hand-written undo.

The whole flow runs offline against the emulators (no Fly account, no cost), and points at real Fly + real Sprites by changing two environment variables.

OpPoint
fly-deploythe happy path — checkpoint, build, deploy, verify, tear down
fly-deploy-guardedthe rollback climax — a risky follow-up step fails and the Sprite rewinds

It composes two smaller examples: local-fly (App + Machine → flaps via flyApply) and sprites-agent-task (checkpoint-as-compensation on a Sprite).

src/infra.ts is one Fly App and one Machine — the smallest complete deploy, authored declaratively:

import { App, Machine, MachineConfig, MachineGuest, Fly } from "@intentius/chant-lexicon-fly";
// org_slug is required by the Machines API (real Fly rejects app creation
// without it). Fly.OrgSlug resolves from FLY_ORG at build time, default
// "personal".
const app = new App({ name: "fly-deploy-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 }),
}),
});

build:fly serializes these into the flaps create bodies flyApply POSTs: the App into POST /v1/apps { app_name, org_slug }, the Machine into POST /v1/apps/fly-deploy-demo/machines { name, region, config }. The serializer stamps managed-by: chant into config.metadata, the ownership marker the owned-only prune reads back.

  • Node.js and npm.
  • Docker (for the offline run — it boots the spritzer and mudflaps emulators in containers). Real mode needs no Docker.

Point the two activity endpoints at the local emulators and run the Op:

Terminal window
cd examples/fly-deploy-rollback
npm install
export FLY_FLAPS_BASE_URL=http://localhost:4280
export SPRITES_BASE_URL=http://localhost:4290
chant run fly-deploy
chant run fly-deploy-guarded

fly-deploy exits ok. The phases run with no raw shell: boot the emulators, create the Sprite sandbox and seed its known-good state, checkpoint it, build the flaps plan, flyApply the App + Machine (waiting each machine to started), verify, then tear down.

fly-deploy-guarded is the same setup, then a risky follow-up step (./risky.sh, run in the Sprite) corrupts the sandbox’s state and exits non-zero. The failing RiskyChange phase triggers the Op-level onFailure Rollback, which restores the known-good checkpoint — rewinding the whole sandbox — then proves the rewind and cleans up. It exits non-zero on purpose:

[phase] Deploy
✓ flyApply(planPath=dist/fly.json) 165ms
[phase] RiskyChange
✗ spriteExec(id=deploy-sandbox, cmd=./risky.sh) 5ms
sprite deploy-sandbox exec "./risky.sh" exited 1: risky.sh: failed
[phase] Rollback
✓ spriteRestore(id=deploy-sandbox, comment=known-good) 3ms
✓ spriteExec(id=deploy-sandbox, cmd=cat /work/state) 3ms
[outcome] state=known-good
✓ spriteDestroy(id=deploy-sandbox) 1ms
✓ flapsDown() 149ms
✓ spritesDown() 132ms
Op "fly-deploy-guarded" failed after 1.4s

A Sprite checkpoint is the transactional boundary, so recovery is a restore, not a hand-written inverse action. The checkpoint comment and Sprite id are static strings, so nothing has to be threaded out of a prior phase.

Drop the two emulator overrides and provide tokens. No code change:

Terminal window
unset FLY_FLAPS_BASE_URL # flyApply falls through to real Fly (api.machines.dev)
unset SPRITES_BASE_URL # the sprite steps fall through to real Sprites
export FLY_API_TOKEN=... # a Fly deploy token
export SPRITES_API_TOKEN=... # a Sprites token
chant run fly-deploy

Each env var also gates its emulator, so real mode boots no container (no Docker required) and the phase list is just Sandbox → Checkpoint → Build → Deploy → Teardown. The deploy is orchestrated by the Op in both modes; the Sprite is the rollback boundary, not the deploy host. See the example README for the full real-vs-modeled breakdown.