Fly.io Apps skill for Claude Code
Machines, volumes and regions configured on purpose. Claude writes fly.toml with real health checks, request concurrency limits and deliberate auto stop settings, runs at least two Machines for anything that must stay up, and explains that each volume lives on one host and needs its own backups. Migrations run as release commands that are safe during rolling deploys, and rollback is a redeploy of a recorded image.
- name
fly-io - size193 lines · ≈2,000 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Deploys and operates apps on Fly.io - fly.toml configuration, http_service and concurrency limits, health checks, auto stop and start of Machines, min machines, VM sizing, multi-region placement, volumes and their single-host limits, secrets, release commands for migrations, deploy strategies, private networking, Postgres options, logs, SSH and rollback. Use when launching an app on Fly, editing fly.toml, adding volumes or regions, fixing failed deploys, health checks or machines that do not wake, or planning backups and scaling on Fly.io. Try asking
- Deploy this Rails app to Fly with Postgres and a release migration
- Our Fly app loses its SQLite data sometimes. What is going on?
- Make this Fly app scale to zero but wake fast for the first request
Use it when
- Launching or reconfiguring an app on Fly.io
- Adding volumes, regions or more Machines
- Deploys fail health checks or Machines do not wake up
- Planning backups, secrets and rollback on Fly
Skip it when
- The app is a frontend or Next.js site on Vercel — use vercel-deploys
- You run SQLite replication in depth — use sqlite-local-first
How to check the result
fly config validate passes, fly status shows healthy Machines on the new version in each region, health URLs return 200, and the previous image reference is recorded for rollback.
The common mistake it prevents
Treating a Fly volume like replicated cloud storage, when it is one disk on one host that needs snapshots and app-level replication.
A solid Fly deployment has a fly.toml that declares the service, health checks and
concurrency limits; runs at least two Machines for anything that must stay up; treats
each volume as a single disk on a single host with its own backups; keeps secrets in
fly secrets; and runs migrations in a release command that blocks bad deploys.
Workflow
- [ ] 1. fly status / fly config show: read current app, regions, machines, volumes
- [ ] 2. Review fly.toml: build, env, http_service, checks, vm, mounts, deploy
- [ ] 3. Decide machine count, regions, auto stop/start, sizing
- [ ] 4. Volumes only for state that cannot live in a managed database or object storage
- [ ] 5. Secrets; release_command for migrations
- [ ] 6. fly deploy; watch health checks; know the rollback
- [ ] 7. Logs, metrics, backups
fly.toml baseline
app = "orders-api"
primary_region = "fra"
[build]
# Dockerfile in repo root is used by default; or: image = "registry/app:tag"
[env]
PORT = "8080"
LOG_LEVEL = "info"
[deploy]
release_command = "npm run migrate" # runs once in a temporary Machine; failure aborts deploy
strategy = "rolling" # rolling | immediate | canary | bluegreen
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop" # "off" | "stop" | "suspend"
auto_start_machines = true
min_machines_running = 1 # in primary region
[http_service.concurrency]
type = "requests" # "connections" for raw TCP/websockets
soft_limit = 200 # proxy prefers other machines above this
hard_limit = 250 # proxy stops sending new requests above this
[[http_service.checks]]
grace_period = "10s"
interval = "15s"
timeout = "2s"
method = "GET"
path = "/healthz"
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
Validate before deploying: fly config validate. Check the effective config of a running
app: fly config show.
Machines, scaling, regions
| Need | Setting |
|---|---|
| Survive a host failure | At least 2 Machines (fly scale count 2); fly deploy creates 2 by default for new apps with services |
| Scale to zero for low-traffic apps | auto_stop_machines = "stop" (or "suspend" for faster wake), min_machines_running = 0; first request pays the start latency |
| Always warm | min_machines_running = 1 or more |
| Predictable CPU | performance-* sizes instead of shared-cpu-* (shared CPU has burst quotas) |
| Users on several continents | Add regions: fly scale count 3 --region fra,iad,syd; keep the database close to writers |
fly scale show
fly scale vm shared-cpu-2x --memory 1024
fly scale count 2 --region fra
fly machine list
fly regions list
The concurrency soft_limit/hard_limit drive both load balancing and autostart:
machines start when existing ones exceed the soft limit. Set limits from load tests;
defaults are rarely right for your app.
Volumes
- A volume is a slice of NVMe on one physical host, attached to one Machine, in one region. It is not replicated. If the host fails, that volume’s data is unavailable or lost until restored from a snapshot.
- One volume per Machine; to run two Machines with volumes, create two volumes and replicate data at the application layer (or use LiteFS/Litestream for SQLite).
- Daily snapshots are taken automatically with limited retention; configure retention and also back up to object storage for anything important.
fly volumes create data --region fra --size 10 # GB
fly volumes list
fly volumes extend <volume-id> --size 20 # grow only, never shrink
fly volumes snapshots list <volume-id>
[mounts]
source = "data"
destination = "/data"
Prefer a managed database (Fly Managed Postgres, or an external provider) and Tigris or
another object store for files, over self-run databases on volumes. Legacy unmanaged
fly postgres clusters are apps you operate yourself, including upgrades and failover.
Destroying volumes or apps requires explicit user confirmation.
Secrets and config
fly secrets set DATABASE_URL="postgres://..." STRIPE_KEY=sk_live_... # triggers a rolling restart
fly secrets set --stage FEATURE_X_KEY=... # apply on next deploy
fly secrets list # names and digests only
fly secrets unset OLD_KEY
Non-secret config belongs in [env] in fly.toml so it is versioned with the code.
Deploys and rollback
fly deploy # build remotely, release_command, rolling update
fly deploy --strategy bluegreen # needs health checks; full parallel set then switch
fly deploy --image registry.fly.io/orders-api:deployment-01J... # redeploy a previous image
fly releases --image # history with image references
fly status # machine states and versions
Rollback means redeploying a previous image reference from fly releases --image.
Migrations in release_command must be backward compatible with the running version
(expand/contract), because old Machines keep serving during a rolling deploy.
Production deploys require user confirmation.
Networking
- Private network (6PN): apps in the same organization reach each other at
<app>.internal(IPv6). Bind internal services to[::]orfly-local-6pn. - Flycast (
fly ips allocate-v6 --private) gives a private address that goes through the Fly proxy, so autostart and load balancing work for internal services. fly proxy 5432 -a orders-dbtunnels a private port to your machine for admin work.- Custom domains:
fly certs add api.example.com, then create the DNS records it prints. - The
fly-replayresponse header lets a read-replica region forward write requests to the primary region.
Operations
fly logs -a orders-api # live logs
fly ssh console -a orders-api # shell in a running Machine
fly machine restart <id>
fly checks list
Metrics live in the managed Grafana linked from the app dashboard. Ship logs to a provider with the Fly log shipper app if you need retention or search beyond the live tail.
Diagnosing
| Symptom | Likely cause |
|---|---|
| Deploy hangs then fails health checks | App listens on 127.0.0.1 or the wrong port vs internal_port; slow start beyond grace_period |
| “No machines available” or 502 bursts | All machines at hard_limit, or stopped with auto_start_machines = false |
| Data missing after deploy | Writing outside the mounted volume path; or new Machine got a new empty volume |
| Machine will not start in region | Volume pinned to another region or host capacity; create volume in target region |
OOM restarts (exit code 137, “out of memory” in logs) | Increase memory or fix leak; add swap via swap_size_mb only as a buffer |
Gotchas
fly launchwrites a fly.toml with defaults (auto stop on, min 0); review before production.- Scaling with volumes requires a volume per new Machine in that region;
fly scale countcan create them, but data does not copy. - Release commands do not have access to volumes.
- Shared CPU Machines get throttled under sustained load; check CPU quota metrics before blaming code.
Verification
fly config validatepasses;fly statusshows the expected Machine count in each region, all on the new version, checks passing.curl -i https://<app>.fly.dev/healthzreturns 200; with scale-to-zero, the first request after idle still succeeds within the acceptable start time.- For volumes: snapshots listed, and a restore has been tested to a scratch volume.
- Previous image reference recorded so rollback is a single
fly deploy --image ....
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.