Fly.io runs Dockerized apps globally on anycast infrastructure — fly launch scaffolds a fly.toml and Dockerfile. fly deploy builds and deploys with zero-downtime rolling updates. fly.toml configures app, primary_region, [build], [[services]] (ports, [[services.tcp_checks]]), [env], and [[mounts]] for volumes. Machines API: POST https://api.machines.dev/v1/apps/{app}/machines starts ephemeral VMs with a custom Docker image in any region — useful for running AI jobs or preview environments. fly volumes create data --size 10 --region iad creates a persistent disk. fly postgres create provisions a managed Postgres cluster; fly postgres attach adds DATABASE_URL to app secrets. Private networking: every Fly app gets APP_NAME.internal DNS on the fdaa::/8 IPv6 network — services talk to each other without public internet. Secrets: fly secrets set KEY=VALUE injects env vars, fly secrets import < .env bulk loads. fly scale count 3 adds replicas; fly scale vm performance-2x upgrades hardware. fly logs streams live logs. fly ssh console opens a shell. Claude Code generates Fly.io fly.toml, Dockerfiles, Machines API integrations, and GitHub Actions CI/CD pipelines.
CLAUDE.md for Fly.io
## Fly.io Stack
- CLI: fly (Flyctl) — all operations via fly commands
- Init: fly launch — creates fly.toml + Dockerfile scaffold
- Deploy: fly deploy — builds Docker image, pushes, rolling restart
- Secrets: fly secrets set KEY=VALUE (or fly secrets import < .env for bulk)
- Scale: fly scale count 2 (replicas), fly scale vm shared-cpu-2x (size)
- Volumes: fly volumes create vol-name --size 10 --region iad; mount in [[mounts]] in fly.toml
- Postgres: fly postgres create --name myapp-db --region iad; fly postgres attach --app myapp
- Logs: fly logs --app myapp
- SSH: fly ssh console --app myapp
fly.toml Configuration
# fly.toml — production Next.js app on Fly.io
app = "my-nextjs-app"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
PORT = "3000"
# Sensitive values go in: fly secrets set OPENAI_API_KEY=sk-...
[[services]]
internal_port = 3000
protocol = "tcp"
[[services.ports]]
port = 80
handlers = ["http"]
force_https = true
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[services.concurrency]
type = "connections"
hard_limit = 200
soft_limit = 150
[[services.http_checks]]
interval = "10s"
timeout = "5s"
grace_period = "30s"
method = "GET"
path = "/api/health"
[[mounts]]
source = "app_data"
destination = "/data"
Dockerfile for Next.js
# Dockerfile — optimized multi-stage Next.js build for Fly.io
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system nodejs && adduser --system --ingroup nodejs nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s \
CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
Fly Machines API Client
// lib/fly/machines.ts — Fly Machines API for ephemeral GPU/CPU VMs
const FLY_API_TOKEN = process.env.FLY_API_TOKEN!
const FLY_APP_NAME = process.env.FLY_APP_NAME!
const MACHINES_API = "https://api.machines.dev/v1"
export type MachineConfig = {
image: string // Docker image
region?: string // e.g. "iad", "lhr", "nrt"
cpus?: number
memory?: number // MB
env?: Record<string, string>
cmd?: string[]
}
export type Machine = {
id: string
state: "created" | "started" | "stopping" | "stopped" | "destroying" | "destroyed"
region: string
image_ref: { registry: string; repository: string; tag: string }
}
async function flyFetch(path: string, init: RequestInit = {}): Promise<Response> {
return fetch(`${MACHINES_API}${path}`, {
...init,
headers: {
Authorization: `Bearer ${FLY_API_TOKEN}`,
"Content-Type": "application/json",
...((init.headers as Record<string, string>) ?? {}),
},
})
}
/** Start a new Machine (ephemeral VM) */
export async function createMachine(config: MachineConfig): Promise<Machine> {
const res = await flyFetch(`/apps/${FLY_APP_NAME}/machines`, {
method: "POST",
body: JSON.stringify({
region: config.region ?? "iad",
config: {
image: config.image,
guest: {
cpus: config.cpus ?? 1,
memory_mb: config.memory ?? 256,
cpu_kind: "shared",
},
env: config.env ?? {},
...(config.cmd ? { cmd: config.cmd } : {}),
auto_destroy: true, // destroy when process exits
restart: { policy: "no" },
},
}),
})
if (!res.ok) throw new Error(`Create machine failed: ${await res.text()}`)
return res.json()
}
/** Wait for a Machine to reach a state */
export async function waitForMachine(
machineId: string,
state: "started" | "stopped" | "destroyed",
timeoutMs = 60_000,
): Promise<void> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const res = await flyFetch(`/apps/${FLY_APP_NAME}/machines/${machineId}`)
const machine: Machine = await res.json()
if (machine.state === state) return
await new Promise((r) => setTimeout(r, 2000))
}
throw new Error(`Machine ${machineId} did not reach state "${state}" in ${timeoutMs}ms`)
}
/** Stop and destroy a Machine */
export async function destroyMachine(machineId: string): Promise<void> {
await flyFetch(`/apps/${FLY_APP_NAME}/machines/${machineId}/stop`, { method: "POST" })
await waitForMachine(machineId, "stopped")
await flyFetch(`/apps/${FLY_APP_NAME}/machines/${machineId}`, { method: "DELETE" })
}
/** List running machines */
export async function listMachines(): Promise<Machine[]> {
const res = await flyFetch(`/apps/${FLY_APP_NAME}/machines`)
if (!res.ok) throw new Error(`List machines failed: ${await res.text()}`)
return res.json()
}
GitHub Actions CI/CD
# .github/workflows/deploy.yml — automated Fly.io deploy on push
name: Deploy to Fly.io
on:
push:
branches: [main]
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
concurrency: deploy-group # prevent simultaneous deploys
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy
run: fly deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
For the Railway alternative when needing a simpler UI-driven platform with automatic Postgres provisioning, deploy previews on PR, and a more beginner-friendly experience — Railway abstracts away more infrastructure while Fly.io gives full Dockerfile control, Machines API access, and lower per-request latency via anycast global routing, see the Railway guide. For the Render alternative when wanting a Heroku-like PaaS with web services, background workers, cron jobs, and managed Postgres all in a single dashboard without ever touching a Dockerfile — Render is the easiest managed platform while Fly.io rewards developers who want Docker-native deployments at the edge with persistent SSHable VMs, see the Render guide. The Claude Skills 360 bundle includes Fly.io skill sets covering fly.toml, Machines API, and multi-region deployment. Start with the free tier to try global deployment generation.