Skip to main content
Technology & EngineeringDeployment Patterns412 lines

fly-io-deployment

Complete guide to deploying applications on Fly.io, covering flyctl CLI usage, Dockerfile-based deployments, fly.toml configuration, persistent volumes, horizontal and vertical scaling, multi-region deployments, managed Postgres and Redis, private networking, and auto-scaling strategies.

Quick Summary35 lines
```bash
curl -L https://fly.io/install.sh | sh

## Key Points

- **`auto_stop_machines`**: Set to `'stop'` to scale to zero when idle (saves money). Set to `'off'` for always-on services.
- **`min_machines_running`**: Keep at least N machines always running. Set to 0 for full scale-to-zero.
- **`primary_region`**: Where your app lives by default. Choose the region closest to your database.
- Volumes are pinned to a specific region and host.
- Only one machine can mount a volume at a time.
- Volumes survive deploys and restarts.
- Snapshots are taken daily (retained for 5 days).
1. **Not setting `min_machines_running`**: Scale-to-zero means cold starts of 2-5 seconds. Keep at least 1 machine running for latency-sensitive apps.
2. **Storing state on the filesystem without volumes**: Machine restarts lose all non-volume data.
3. **Using shared-cpu for CPU-bound work**: The shared CPU is fine for web servers but thrashes under sustained compute.
4. **Ignoring health checks**: Without health checks, Fly can't detect and replace unhealthy machines.
5. **Multi-region without replay headers**: Write requests to read replicas will fail silently without the `fly-replay` pattern.

## Quick Example

```bash
# Create a volume
fly volumes create data --size 10 --region iad

# List volumes
fly volumes list
```

```toml
[mounts]
  source = 'data'
  destination = '/data'
```
skilldb get deployment-patterns-skills/fly-io-deploymentFull skill: 412 lines
Paste into your CLAUDE.md or agent config

Deploying to Fly.io

Getting Started with flyctl

# Install flyctl
curl -L https://fly.io/install.sh | sh

# Authenticate
fly auth login

# Create a new app
fly launch

# Deploy
fly deploy

fly launch scaffolds your project with a Dockerfile and fly.toml. It auto-detects frameworks (Rails, Django, Node, Go, Elixir, etc.) and generates an appropriate Dockerfile.

Dockerfile for Fly.io

Node.js Multi-Stage Build

FROM node:20-slim AS base
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --production=false

FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
RUN npm prune --production

FROM base AS runtime
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./

EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "dist/server.js"]

Go Application

FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=build /app/server /server
EXPOSE 8080
CMD ["/server"]

Anti-pattern: Using a full base image in the runtime stage. Always use slim/alpine or distroless for production.

fly.toml Configuration

app = 'my-app'
primary_region = 'iad'

[build]

[env]
  PORT = '3000'
  NODE_ENV = 'production'
  LOG_LEVEL = 'info'

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 1
  processes = ['app']

  [http_service.concurrency]
    type = 'connections'
    hard_limit = 250
    soft_limit = 200

[[vm]]
  memory = '512mb'
  cpu_kind = 'shared'
  cpus = 1

[checks]
  [checks.health]
    port = 3000
    type = 'http'
    interval = '15s'
    timeout = '5s'
    grace_period = '10s'
    method = 'GET'
    path = '/health'

Key Configuration Sections

  • auto_stop_machines: Set to 'stop' to scale to zero when idle (saves money). Set to 'off' for always-on services.
  • min_machines_running: Keep at least N machines always running. Set to 0 for full scale-to-zero.
  • primary_region: Where your app lives by default. Choose the region closest to your database.

Secrets Management

# Set secrets (triggers redeploy)
fly secrets set DATABASE_URL="postgresql://user:pass@db.internal:5432/mydb"
fly secrets set SESSION_SECRET="$(openssl rand -hex 32)"

# List secrets (values are hidden)
fly secrets list

# Remove a secret
fly secrets unset OLD_SECRET

# Import from file
cat .env.production | fly secrets import

Anti-pattern: Putting secrets in [env] in fly.toml. That section is for non-sensitive configuration only. Use fly secrets for anything sensitive.

Volumes (Persistent Storage)

# Create a volume
fly volumes create data --size 10 --region iad

# List volumes
fly volumes list

Mount in fly.toml:

[mounts]
  source = 'data'
  destination = '/data'

Volume Considerations

  • Volumes are pinned to a specific region and host.
  • Only one machine can mount a volume at a time.
  • Volumes survive deploys and restarts.
  • Snapshots are taken daily (retained for 5 days).
# Extend volume size
fly volumes extend vol_abc123 --size 20

# Snapshot management
fly volumes snapshots list vol_abc123

Anti-pattern: Using volumes for data that should be in a database. Volumes are for SQLite, file uploads, and caches. For structured data, use Postgres.

Scaling

Vertical Scaling

# Scale machine size
fly scale vm shared-cpu-2x
fly scale vm performance-2x
fly scale memory 1024

Horizontal Scaling

# Scale to 3 machines in primary region
fly scale count 3

# Scale per region
fly scale count iad=3 cdg=2 nrt=1

Auto-scaling

Configure in fly.toml:

[http_service]
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 1

  [http_service.concurrency]
    type = 'requests'
    hard_limit = 100
    soft_limit = 80

When concurrency exceeds soft_limit, Fly starts additional machines. When traffic drops, machines stop (if auto_stop_machines is enabled).

Multi-Region Deployment

# Add regions
fly regions add cdg nrt sin

# Scale machines to those regions
fly scale count iad=2 cdg=2 nrt=1 sin=1

# Check region distribution
fly regions list

Read Replicas Pattern

For read-heavy workloads with a single-writer database:

[env]
  PRIMARY_REGION = 'iad'
// In your app, detect region and replay writes to primary
const currentRegion = process.env.FLY_REGION;
const primaryRegion = process.env.PRIMARY_REGION;

if (isWriteRequest(req) && currentRegion !== primaryRegion) {
  // Fly.io replays the request to the primary region
  res.setHeader('fly-replay', `region=${primaryRegion}`);
  return res.status(409).end();
}

Managed Postgres

# Create a Postgres cluster
fly postgres create --name my-app-db --region iad --vm-size shared-cpu-1x --volume-size 10

# Attach to your app (sets DATABASE_URL secret automatically)
fly postgres attach my-app-db

# Connect via proxy
fly postgres connect -a my-app-db

# Add read replicas
fly machine clone -a my-app-db --region cdg

Postgres Backups

# Manual backup via pg_dump through proxy
fly proxy 5432 -a my-app-db &
pg_dump "postgres://user:pass@localhost:5432/mydb" > backup.sql

Anti-pattern: Running production Postgres on a single machine without monitoring. Always set up at least one replica and enable monitoring.

Managed Redis (Upstash)

# Create Redis via Fly (powered by Upstash)
fly redis create --name my-cache --region iad

# Connect to your app
fly redis attach my-cache

This sets REDIS_URL as a secret. Upstash Redis is serverless and per-request priced.

Private Networking

All Fly.io apps in the same organization share a private WireGuard network:

# Access other apps via .internal DNS
my-app-db.internal    -> Postgres
my-cache.internal     -> Redis
my-api.internal       -> Internal API service
# Expose a service only on the private network
[http_service]
  internal_port = 8080
  auto_stop_machines = 'stop'

[[services]]
  protocol = 'tcp'
  internal_port = 8080

  # No public ports = private only

Service Discovery

// Connect to internal services
const dbUrl = `postgres://user:pass@my-app-db.internal:5432/mydb`;
const redisUrl = `redis://default:pass@my-cache.internal:6379`;
const apiUrl = `http://my-api.internal:8080`;

Process Groups

Run multiple process types from one codebase:

[processes]
  app = 'node dist/server.js'
  worker = 'node dist/worker.js'
  cron = 'node dist/cron.js'

[http_service]
  processes = ['app']

[[vm]]
  processes = ['app']
  memory = '512mb'
  cpus = 1

[[vm]]
  processes = ['worker']
  memory = '1gb'
  cpus = 2

[[vm]]
  processes = ['cron']
  memory = '256mb'
  cpus = 1

Deployment Strategies

# Standard rolling deploy
fly deploy

# Immediate deployment (replace all at once)
fly deploy --strategy immediate

# Canary deployment
fly deploy --strategy canary

# Deploy a specific Docker image
fly deploy --image registry.example.com/my-app:v1.2.3

Rollback

# List recent releases
fly releases

# Rollback to previous release
fly deploy --image registry.fly.io/my-app:deployment-abc123

Monitoring and Debugging

# View logs
fly logs

# SSH into a running machine
fly ssh console

# Check machine status
fly status

# Run one-off commands
fly ssh console -C "node dist/migrate.js"

# View metrics
fly dashboard

Common Anti-Patterns

  1. Not setting min_machines_running: Scale-to-zero means cold starts of 2-5 seconds. Keep at least 1 machine running for latency-sensitive apps.
  2. Storing state on the filesystem without volumes: Machine restarts lose all non-volume data.
  3. Using shared-cpu for CPU-bound work: The shared CPU is fine for web servers but thrashes under sustained compute.
  4. Ignoring health checks: Without health checks, Fly can't detect and replace unhealthy machines.
  5. Multi-region without replay headers: Write requests to read replicas will fail silently without the fly-replay pattern.

Deployment Checklist

  • fly.toml configured with correct regions and scaling
  • Dockerfile uses multi-stage builds with minimal runtime image
  • Secrets set via fly secrets, not in fly.toml
  • Health check endpoint implemented and configured
  • Volumes created for persistent storage needs
  • Auto-scaling concurrency limits tuned for your workload
  • Private networking used for inter-service communication
  • Monitoring and alerting configured

Install this skill directly: skilldb add deployment-patterns-skills

Get CLI access →

Related Skills

database-deployment

Comprehensive guide to database deployment for web applications, covering managed database services (PlanetScale, Neon, Supabase, Turso), migration strategies, connection pooling, backup and restore procedures, data seeding, and schema management best practices for production environments.

Deployment Patterns539L

docker-deployment

Comprehensive guide to using Docker for production deployments, covering multi-stage builds, .dockerignore optimization, layer caching strategies, health checks, Docker Compose for local development, container registries, and security scanning best practices.

Deployment Patterns479L

github-actions-cd

Comprehensive guide to implementing continuous deployment with GitHub Actions, covering deploy workflows, environment protection rules, secrets management, matrix builds, dependency caching, artifact management, and deploying to multiple targets including Vercel, Fly.io, AWS, and container registries.

Deployment Patterns469L

monitoring-post-deploy

Comprehensive guide to post-deployment monitoring for web applications, covering uptime checks, error tracking with Sentry, application performance monitoring, log aggregation, alerting strategies, public status pages, and incident response procedures for production systems.

Deployment Patterns572L

netlify-deployment

Complete guide to deploying web applications on Netlify, covering build settings, deploy previews, serverless and edge functions, forms, identity, redirects and rewrites, split testing, and environment variable management for production workflows.

Deployment Patterns399L

railway-deployment

Complete guide to deploying applications on Railway, covering project setup, environment variable management, services and databases (Postgres, Redis, MySQL), persistent volumes, monorepo support, private networking between services, and scheduled cron jobs.

Deployment Patterns434L