Skip to content

feat: run preview cobalt on fly through alchemy - #200

Open
flamboh wants to merge 11 commits into
alchemy/02-cloudflare-stackfrom
alchemy/03-cobalt-fly
Open

flamboh wants to merge 11 commits into
alchemy/02-cloudflare-stackfrom
alchemy/03-cobalt-fly

Conversation

@flamboh

@flamboh flamboh commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Warning

🤖 Claude Opus 5.5 on behalf of Oliver. Deploy-tested on a throwaway stage and torn down. Pins an unreleased Alchemy build and moves to Bun 1.4.2, so Workers Builds needs BUN_VERSION=1.4.2 before merge. Stacked on #198.

ELI5

Each test copy of the site now also gets its own private Cobalt download server on Fly. Alchemy creates and deletes it along with everything else, so one command sets up or tears down the whole thing.

Why

Layer 3 of the Alchemy migration (#197#198 → this). Until now, previews shared production's Cobalt and its API key, and Cobalt was deployed separately with flyctl. Alchemy's main branch now supports a 300 s graceful shutdown for Fly machines, which makes one alchemy deploy for Cloudflare and Fly possible.

Implementation

  • Versions: Alchemy is pinned to the main preview build 4eae83e. That build only installs with Bun ≥ 1.4, so packageManager is now bun@1.4.2 and the lockfile is regenerated. It swaps to the next npm beta once it ships.
  • Per stage, in alchemy.run.ts:
    • A Fly.App named tagium-cobalt-<stage>: never tagium-cobalt, with a 30-character guard and prod stages refused. The org comes from the org-scoped token.
    • A shared IPv4 plus IPv6 address.
    • Docker.Image, which builds Dockerfile.cobalt with --provenance=false (so identical builds keep the same digest) and pushes it to the stage app's registry.
    • Fly.Machine, running the image by digest. It mirrors fly.cobalt.toml: lax, 1 GB shared, suspend when idle, the /readyz check, and SIGTERM with a 300 s drain.
  • Blue/green deploys, not rolling:
    • Alchemy health-checks a fresh machine set, cordons the old machines, and waits for their drain before destroying them.
    • Rolling deploys failed the test: Alchemy hard-codes a 60 s wait after updating a machine, so a deploy made while a download was draining timed out and left the second machine un-updated. (Upstream fix: scale those waits with shutdown.timeout.)
    • Machine IDs now change on every deploy. That's fine, because tunnel URLs are only used for seconds after they're resolved.
  • Cobalt API key: each stage generates its own key and passes it to Cobalt as a data: URL keys file with API_AUTH_REQUIRED=1. The Worker gets the same key, and COBALT_API_URL comes from the Fly app's URL. Previews no longer depend on production's COBALT_API_KEY.
  • CI: preview.yaml needs a FLY_PREVIEW_API_TOKEN secret (org-scoped) and gets Docker from the runner. It logs in to registry.fly.io, because the base image lives in production's registry.
  • Unchanged: production Cobalt (fly.cobalt.toml and the ci.yaml Cobalt jobs) and production Cloudflare.

Review

  • Setup:
    1. Install Docker.
    2. Put CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and an org-scoped FLY_API_TOKEN in your env.
    3. echo "$FLY_API_TOKEN" | docker login registry.fly.io -u x --password-stdin
    4. bun run alchemy:deploy --stage dev-you --yes
  • Flows to exercise:
    1. Import a SoundCloud track on the workers.dev URL.
    2. Redeploy after changing a Cobalt env value. Machines should be replaced with no failed imports.
    3. bun run alchemy:destroy --stage dev-you --yes removes the Fly app too.
  • Decisions to weigh:
    • Blue/green deploys take 2–3 minutes instead of about 35 s.
    • A request that resolves on an old machine in the few seconds before it's cordoned can get a 503 when it fetches its tunnel. Rolling restarts have the same window.
    • The per-stage key is plain text in the Machine's env.
    • The preview Fly token can reach production's registry. It needs that to pull the base image.
  • Deploy-tested on stage dev-layer3 (2 machines):
    • Fly app: machines, IPs and /readyz are healthy. Requests without the key → error.api.auth.key.missing.
    • SoundCloud imports work through the Worker: 9 runs with the full byte count (3,422,666 B plus cover). Tunnels stay pinned to the machine that resolved them, and resolves hit both machines.
    • YouTube returns error.api.content.video.unavailable from Fly IPs, identical to production tagium.app.
    • Graceful deploy: a blue/green deploy during a slow tunnel read succeeded in 194 s, with the old machine draining for 53 s before being destroyed.
    • No-change redeploy: image and machines are left alone (23 s). Only the Worker re-uploads.
    • Destroy: removed all 10 resources, including the Fly app. Production tagium-cobalt (machine, image, instance) was untouched.
  • Found while testing and fixed here:
    • The machine's logical ID collided with the app's, so Alchemy never planned any machines.
    • FLY_ORG=personal didn't match Fly's canonical org slug, so Alchemy replaced the app on every deploy. The FLY_ORG variable is removed.
    • Docker's provenance attestations changed the image digest on every build.
  • Known issue, not from this PR: clients reading slower than the machine can lose the last ~1–2 MB when the VM stops right after the proxy's drain finishes. cobalt-machine-proxy.mjs is unchanged from main, so production has the same behavior. Real imports go through the Worker, which reads at full speed.
  • Verified automatically: vp check (0 errors), bun run typecheck, vp test (661) and vp build pass.
  • Before merge: set BUN_VERSION=1.4.2 in Workers Builds, or wait for beta.80 and drop the Bun bump.

Claude Opus 5.5 · Claude Code (T3 Code)

@github-actions

github-actions Bot commented Sep 23, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 6e057b6.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
tagium 6e057b6 Commit Preview URL

Branch Preview URL
Sep 23 2026, 04:09 AM

Fly.Machine("Cobalt") shared the Fly.App logical id, so Alchemy returned the
existing app resource and never created any machines.
Fly reports the canonical org slug, so an alias such as FLY_ORG=personal never
matched and every deploy replaced the Fly app with delete-first.
BuildKit's provenance attestation changes the image id on every build, so
unchanged deploys re-pushed the image and rolled every Cobalt machine.
Rolling updates fail after Alchemy's fixed 60-second wait while Fly drains the
old instance, so a deploy during a long tunnel errored. Blue/green retirement
waits for the SIGTERM drain before destroying old machines.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant