Fly.io skill for Codex
Machines, volumes and regions without surprises. Codex sets up Fly.io apps with a fly.toml that has real readiness checks, sensible auto stop and start, release commands for migrations and a deliberate deploy strategy. It treats volumes as single-host disks that need off-platform backups, routes writes to the primary region, connects services over .internal or Flycast, and records the rollback image before anything risky ships.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- fly-io Installs to ~/.agents/skills/fly-io/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install fly-io The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project fly-io Installs to .agents/skills/fly-io/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/fly-io.zipunzip -o fly-io.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) fly-io PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $fly-io, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Deploys and runs apps on Fly.io with flyctl and fly.toml - Machines, http_service and auto stop/start, health checks, release commands, deploy strategies, secrets, volumes and snapshots, multi-region placement, private networking over .internal and Flycast, scaling and rollback. Use when launching an app on Fly, a deploy hangs on health checks, machines will not stop or start, data on a volume is at risk, you are adding regions, or connecting services privately.
Use it when
- Launching or migrating an app onto Fly.io
- Deploys fail on health checks or machines do not auto stop and start
- An app stores data on a Fly volume
- Adding regions or connecting apps over the private network
Use something else when
- You want a simpler managed PaaS with built-in databases: use paas-deploys
- You are deploying containers to your own servers: use kamal-deploys
How to check the result
fly checks list shows passing checks on every Machine, the public health URL returns 200 over HTTPS, and a backup of any volume data has been restored once.
The mistake it prevents
Assuming a Fly volume is replicated; it lives on one host, so a host failure loses data that has no off-platform backup.
On Fly, an app is a set of Machines (fast-booting VMs) built from your image. A sound
setup has health checks that reflect real readiness, at least two Machines for anything
user-facing, volumes treated as single-host disks with their own backup plan, and secrets
set through fly secrets, never baked into the image.
Inspect first
- Read
AGENTS.md,fly.toml,Dockerfileand the app’s listen address and port. fly status,fly machine listandfly volumes listshow what is actually running. Allflycommands need network andfly auth loginorFLY_API_TOKEN; in a sandbox without network, prepare files and hand commands to the user.- Check which Postgres the app uses: Fly Managed Postgres (
fly mpg), an older self-managed Fly Postgres app, or an external provider. They are operated differently.
Launch
fly launch --no-deploy # detects framework, writes fly.toml and Dockerfile
fly secrets set DATABASE_URL=... SESSION_SECRET=... --stage # stage, apply on next deploy
fly deploy
The process must listen on 0.0.0.0 (or ::) at internal_port. Binding to
127.0.0.1 is the most common cause of “no healthy machines”.
fly.toml baseline
app = "acme-api"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
PORT = "8080"
[deploy]
release_command = "npm run migrate" # runs once in a temporary Machine before rollout
strategy = "rolling"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1
[http_service.concurrency]
type = "requests"
soft_limit = 200
hard_limit = 250
[[http_service.checks]]
grace_period = "10s"
interval = "15s"
timeout = "3s"
method = "GET"
path = "/healthz"
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
| Setting | Guidance |
|---|---|
auto_stop_machines | "stop" saves money; "suspend" resumes faster; "off" for always-on workers |
min_machines_running | Applies to the primary region; set at least 1 for anything latency-sensitive |
concurrency | requests for HTTP apps, connections for raw TCP or WebSockets |
release_command | Migrations only; it has no volume and a failed run aborts the deploy |
| Health check path | Return 200 only when the app can serve: DB pool ready, config loaded |
Deploy strategies
| Strategy | When |
|---|---|
rolling (default) | Replaces Machines one at a time; fine for most stateless apps |
bluegreen | Boots a full new set, waits for health checks, then switches; needs checks defined; not for Machines with volumes |
canary | One new Machine first, then rolling; not for Machines with volumes |
immediate | Replaces everything at once; accept downtime |
Roll back by redeploying a previous image:
fly releases --image # lists releases with image refs
fly deploy --image registry.fly.io/acme-api:deployment-01J... # confirm with user first
Schema migrations do not roll back with the image. Write migrations that the previous release can still run against (expand, then contract).
Volumes
A volume is a slice of NVMe on one physical host, attached to exactly one Machine.
fly volumes create data --region iad --size 10 # GB
fly volumes list
fly volumes snapshots list <volume-id>
fly volumes extend <volume-id> --size 20 # grow only, never shrink
[mounts]
source = "data"
destination = "/data"
Rules:
- Volumes are not replicated. If the host dies, that volume is gone until restored from a snapshot. Daily snapshots exist but are short-retention; for real data run your own backups off Fly (for SQLite, Litestream or LiteFS backups to object storage).
- Two Machines need two volumes; data does not sync between them. Scaling a volume-backed app horizontally needs replication in the application layer.
- Deleting a volume is permanent. Never run
fly volumes destroywithout explicit user confirmation and a verified snapshot.
Regions and scaling
fly platform regions
fly scale count 2 # in primary region
fly scale count 2 --region fra # Machines in an additional region
fly scale vm shared-cpu-2x --memory 1024
fly scale show
Put Machines where the database is. Read replicas in other regions help reads; writes
should go to the primary region (replay a request there with the fly-replay response
header when a non-primary Machine receives a write).
Private networking
- Every app in an organization shares a private IPv6 network.
<app>.internalresolves to all Machines of an app;<region>.<app>.internalnarrows to a region. These addresses bypass the Fly proxy, so autostart does not apply.- Flycast (
fly ips allocate-v6 --private, then<app>.flycast) goes through the proxy, so stopped Machines wake up and load balancing applies. Use it for internal HTTP services that scale to zero. fly proxy 5432 -a acme-dbforwards a private port to your laptop for one-off access.
Debugging
fly logs -a acme-api
fly status --all
fly machine status <id>
fly checks list
fly ssh console -s # pick a Machine and open a shell
fly doctor
| Symptom | Likely cause |
|---|---|
| Deploy waits then fails health checks | Wrong internal_port, bound to localhost, check path slow or needs auth |
| Machines never stop | Open connections, background traffic, or min_machines_running too high |
| First request slow | Cold start after auto stop; raise min_machines_running or use suspend |
| OOM kills in logs | Raise memory; Node may also need --max-old-space-size set below it |
| App cannot reach another app | Using public hostname instead of .internal/.flycast, or listening on IPv4 only |
Verification
-
fly deploycompletes andfly checks listshows passing checks on every Machine. -
curl -I https://<app>.fly.dev/healthzreturns 200 over HTTPS. - At least two Machines for user-facing apps (
fly scale show). - Volume-backed data has a backup that has been restored at least once.
- Rollback image reference recorded before risky deploys.
Gotchas
fly secrets setwithout--stagerestarts Machines immediately.fly launchmay create extra Machines for high availability; checkfly scale showafter the first deploy before assuming one instance.- The release command Machine has no access to volumes; do not run SQLite migrations there. Run them at boot in the app instead.
- Shared CPU Machines get throttled under sustained load; move CPU-bound work to performance sizes.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Fly.io"
short_description: "Machines, volumes and regions without surprises"
default_prompt: "$fly-io Deploy this Node API to Fly.io with health checks and two machines" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.