Back to Skills

fly-deploy

by ianpcook

Deploy applications to Fly.io using flyctl. Handles project detection, fly.toml generation, secrets configuration, and deployment. Use when deploying apps to Fly.io, creating fly.toml files, debugging Fly.io deployment failures, or configuring Fly.io services.

1.0.0
$ npx skills add https://github.com/ianpcook/fly-deploy

Files

SKILL.mdMain
2.8 KB
---
name: fly-deploy
description: Deploy applications to Fly.io using flyctl. Handles project detection, fly.toml generation, secrets configuration, and deployment. Use when deploying apps to Fly.io, creating fly.toml files, debugging Fly.io deployment failures, or configuring Fly.io services.
---

# Fly.io Deployment

Deploy applications to Fly.io from source code or Docker images.

## Prerequisites

Ensure `flyctl` is installed and authenticated:

```bash
# Check installation
fly version

# Authenticate if needed
fly auth login
```

## Deployment Workflow

### 1. Detect Project Type

Examine the project root to determine the stack:

| Indicator | Type | Reference |
|-----------|------|-----------|
| `next.config.*` | Next.js | [references/nextjs.md](references/nextjs.md) |
| `Dockerfile` | Docker | [references/docker.md](references/docker.md) |
| `package.json` (no Next) | Node.js | [references/nodejs.md](references/nodejs.md) |
| `requirements.txt` / `pyproject.toml` | Python | [references/python.md](references/python.md) |
| `index.html` only | Static | [references/static.md](references/static.md) |

Read the appropriate reference file for framework-specific configuration.

### 2. Initialize or Configure

**New app:**
```bash
fly launch --no-deploy
```

This creates `fly.toml`. Review and adjust before deploying.

**Existing app (no fly.toml):**
```bash
fly launch --no-deploy
# Or create fly.toml manually using references/fly-toml.md
```

### 3. Set Secrets

Set environment variables that shouldn't be in fly.toml:

```bash
# Single secret
fly secrets set DATABASE_URL="postgres://..."

# Multiple secrets
fly secrets set KEY1=value1 KEY2=value2

# From .env file
cat .env | fly secrets import
```

Secrets trigger a redeploy. Use `--stage` to batch them:

```bash
fly secrets set --stage KEY1=value1
fly secrets set --stage KEY2=value2
fly secrets deploy
```

### 4. Deploy

```bash
fly deploy
```

**Common flags:**
- `--ha` — High availability (2+ machines, default)
- `--no-ha` — Single machine (dev/staging)
- `--strategy rolling|bluegreen|canary|immediate`
- `--wait-timeout 5m` — Extend for slow builds

### 5. Verify

```bash
# Check status
fly status

# View logs
fly logs

# Open in browser
fly open
```

## Troubleshooting

If deployment fails, consult [references/troubleshooting.md](references/troubleshooting.md) for common errors and fixes.

**Quick checks:**
1. Health check passing? Check `internal_port` matches app's listen port
2. App starting? Check `fly logs` for crash loops
3. Build failing? Check Dockerfile or buildpack compatibility

## Configuration Reference

For detailed fly.toml options, see [references/fly-toml.md](references/fly-toml.md).

**Key settings:**
- `primary_region` — Where machines deploy by default
- `[http_service]` — HTTP/HTTPS configuration
- `[env]` — Non-sensitive environment variables
- `[[vm]]` — CPU/memory sizing
fly-toml.md
4.0 KB
# fly.toml Configuration Reference

## Contents
- [Minimal Example](#minimal-example)
- [App Settings](#app-settings)
- [Build Configuration](#build-configuration)
- [HTTP Service](#http-service)
- [Services (Advanced)](#services-advanced)
- [Environment Variables](#environment-variables)
- [Deploy Settings](#deploy-settings)
- [VM Sizing](#vm-sizing)
- [Volumes](#volumes)
- [Health Checks](#health-checks)

## Minimal Example

```toml
app = "my-app"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
```

## App Settings

```toml
app = "my-app-name"           # Required: app name
primary_region = "ord"        # Required: default region for new machines
```

**Regions:** Run `fly platform regions` for full list. Common: `ord` (Chicago), `iad` (Virginia), `lax` (LA), `fra` (Frankfurt), `sin` (Singapore).

## Build Configuration

**Dockerfile (most common):**
```toml
[build]
  dockerfile = "Dockerfile"     # Default: Dockerfile in root
  # dockerfile = "deploy/Dockerfile"  # Custom path
```

**Build arguments:**
```toml
[build.args]
  NODE_ENV = "production"
```

**Pre-built image:**
```toml
[build]
  image = "nginx:alpine"
```

**Buildpacks (no Dockerfile):**
```toml
[build]
  builder = "paketobuildpacks/builder:base"
```

## HTTP Service

For apps serving HTTP/HTTPS on ports 80/443:

```toml
[http_service]
  internal_port = 8080          # Port your app listens on
  force_https = true            # Redirect HTTP → HTTPS
  auto_stop_machines = "stop"   # Stop idle machines (or "off", "suspend")
  auto_start_machines = true    # Start on incoming request
  min_machines_running = 0      # Keep N machines always running

[http_service.concurrency]
  type = "requests"             # or "connections"
  soft_limit = 200              # Start routing elsewhere
  hard_limit = 250              # Stop accepting
```

**auto_stop_machines options:**
- `"off"` — Never stop (always running, higher cost)
- `"stop"` — Stop after idle period (cold starts on next request)
- `"suspend"` — Suspend (faster wake than stop)

## Services (Advanced)

For non-HTTP protocols or multiple ports:

```toml
[[services]]
  internal_port = 5432
  protocol = "tcp"

[[services.ports]]
  port = 5432
  handlers = ["tls"]
```

## Environment Variables

Non-sensitive values (secrets go via `fly secrets set`):

```toml
[env]
  NODE_ENV = "production"
  LOG_LEVEL = "info"
  PORT = "8080"
```

## Deploy Settings

```toml
[deploy]
  release_command = "npm run db:migrate"  # Run before deploy
  strategy = "rolling"                    # rolling|bluegreen|canary|immediate
```

**Release command notes:**
- Runs in temporary machine with no volumes
- 5 minute timeout (configurable)
- Non-zero exit stops deployment
- Great for database migrations

```toml
[deploy]
  release_command = "python manage.py migrate"
  release_command_timeout = "10m"
```

## VM Sizing

```toml
[vm]
  size = "shared-cpu-1x"    # Default
  memory = "512mb"          # Override memory
```

**Sizes:**
- `shared-cpu-1x` — 1 shared CPU, 256MB (default)
- `shared-cpu-2x` — 2 shared CPU, 512MB
- `shared-cpu-4x` — 4 shared CPU, 1GB
- `performance-1x` — 1 dedicated CPU, 2GB
- `performance-2x` — 2 dedicated CPU, 4GB

Scale via CLI: `fly scale vm shared-cpu-2x`

## Volumes

Persistent storage:

```toml
[mounts]
  source = "myapp_data"
  destination = "/data"
  initial_size = "1gb"
```

Create volume first: `fly volumes create myapp_data --size 1 --region ord`

## Health Checks

HTTP health check:

```toml
[[http_service.checks]]
  grace_period = "10s"      # Wait after start before checking
  interval = "30s"          # Time between checks
  method = "GET"
  path = "/health"          # Health endpoint
  timeout = "5s"            # Max response time
```

**Common issues:**
- `grace_period` too short — App hasn't started yet
- Wrong `path` — Returns 404, fails health check
- `internal_port` mismatch — App listens on different port
troubleshooting.md
5.8 KB
# Troubleshooting Fly.io Deployments

## Contents
- [Health Check Failures](#health-check-failures)
- [Build Failures](#build-failures)
- [App Crashes](#app-crashes)
- [Connection Issues](#connection-issues)
- [Secrets and Environment](#secrets-and-environment)
- [Release Command Failures](#release-command-failures)

## Health Check Failures

**Symptom:** Deploy hangs, then fails with health check timeout.

### Port Mismatch

**Error:** Health check never passes, app seems to start fine.

**Cause:** `internal_port` in fly.toml doesn't match the port your app listens on.

**Fix:**
```bash
# Check what port your app uses
grep -r "listen\|PORT\|port" --include="*.js" --include="*.ts" --include="*.py"

# Update fly.toml
[http_service]
  internal_port = 3000  # Match your app
```

### Grace Period Too Short

**Error:** Health check fails immediately after deploy.

**Cause:** App takes longer to start than `grace_period` allows.

**Fix:**
```toml
[[http_service.checks]]
  grace_period = "30s"  # Increase from default 10s
  interval = "15s"
  timeout = "5s"
  path = "/health"
```

### Missing Health Endpoint

**Error:** Health check returns 404.

**Cause:** App doesn't have a `/health` endpoint (or configured path).

**Fix:** Either add an endpoint or change the check path:
```toml
[[http_service.checks]]
  path = "/"  # Use root if no health endpoint
```

### App Binding to localhost

**Error:** Health check times out, but app works locally.

**Cause:** App binds to `127.0.0.1` instead of `0.0.0.0`.

**Fix:** Configure app to listen on all interfaces:
```bash
# Node.js
server.listen(PORT, '0.0.0.0')

# Python Flask
app.run(host='0.0.0.0', port=PORT)

# Python Uvicorn
uvicorn main:app --host 0.0.0.0 --port $PORT
```

## Build Failures

### Dockerfile Not Found

**Error:** `Error: Dockerfile not found`

**Fix:**
```toml
[build]
  dockerfile = "Dockerfile"  # Ensure path is correct
  # Or use absolute path from repo root
  dockerfile = "./deploy/Dockerfile"
```

### Build Timeout

**Error:** Build killed after timeout.

**Fix:**
```bash
fly deploy --wait-timeout 10m
```

Or in fly.toml:
```toml
[deploy]
  wait_timeout = "10m"
```

### Out of Memory During Build

**Error:** Build OOM killed.

**Fix:** Use remote builder (default) or increase local resources:
```bash
fly deploy --remote-only
```

### npm/yarn Install Fails

**Error:** Package installation fails in Dockerfile.

**Common fixes:**
```dockerfile
# Ensure package-lock.json is copied
COPY package*.json ./
RUN npm ci --only=production

# Or for yarn
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production
```

## App Crashes

### Check Logs First

```bash
fly logs --app my-app

# More context
fly logs --app my-app | head -100
```

### Missing Environment Variables

**Symptom:** App crashes immediately with "undefined" or config errors.

**Fix:**
```bash
# List current secrets
fly secrets list

# Set missing ones
fly secrets set DATABASE_URL="..." API_KEY="..."
```

### Memory Issues

**Symptom:** App killed with OOM.

**Fix:**
```bash
fly scale memory 512  # Increase to 512MB
# Or
fly scale vm shared-cpu-2x
```

### Process Exits Immediately

**Symptom:** Machine starts then stops right away.

**Causes:**
1. Missing CMD/ENTRYPOINT in Dockerfile
2. App exits without error (no long-running process)
3. Crash before any logging

**Debug:**
```bash
# Check recent logs
fly logs

# SSH into a machine for debugging
fly ssh console
```

## Connection Issues

### App Not Accessible

**Symptom:** `fly open` shows error or timeout.

**Checklist:**
1. Check app is running: `fly status`
2. Check machines exist: `fly machines list`
3. Check IP allocation: `fly ips list`
4. If no IPs: `fly ips allocate-v4 --shared`

### HTTPS Redirect Loop

**Symptom:** Browser shows redirect loop.

**Cause:** App also redirects to HTTPS, double redirect.

**Fix:** Let Fly.io handle HTTPS, disable in app:
```toml
[http_service]
  force_https = true  # Fly handles this
```

And remove HTTPS redirect from app code.

### App Works Then Stops

**Symptom:** App responds, then becomes unavailable.

**Cause:** `auto_stop_machines` is enabled, machine stopped due to inactivity.

**Options:**
```toml
[http_service]
  auto_stop_machines = "off"    # Never stop (costs more)
  # Or
  min_machines_running = 1      # Keep 1 always running
```

## Secrets and Environment

### Secrets Not Available

**Symptom:** App can't read environment variable that was set.

**Checks:**
```bash
# Verify secret is set
fly secrets list

# Secrets require redeploy to take effect
fly deploy
```

### Secrets Visible in Logs

**Problem:** Sensitive values appearing in logs.

**Fix:** Never log environment variables directly. Sanitize logs.

## Release Command Failures

### Timeout

**Error:** Release command timed out.

**Fix:**
```toml
[deploy]
  release_command = "npm run migrate"
  release_command_timeout = "10m"  # Increase from 5m default
```

### Database Connection Fails

**Error:** Release command can't connect to database.

**Causes:**
1. DATABASE_URL secret not set
2. Database not accessible from Fly.io network
3. Database requires IP allowlist

**Fixes:**
```bash
# Check secret exists
fly secrets list | grep DATABASE

# For external databases, ensure Fly.io IPs are allowed
# Or use Fly Postgres which is on the same private network
```

### Command Not Found

**Error:** `release_command: command not found`

**Cause:** Command not available in Docker image.

**Fix:** Ensure command is installed in Dockerfile:
```dockerfile
RUN npm install  # Ensures npm scripts work
# Or
RUN pip install -r requirements.txt  # Ensures Python deps available
```

## Quick Diagnostic Commands

```bash
# Overall status
fly status

# Machine list and states
fly machines list

# Recent logs
fly logs

# SSH into running machine
fly ssh console

# Check allocated IPs
fly ips list

# Check secrets
fly secrets list

# Check current config
fly config show
```
nextjs.md
2.8 KB
# Next.js on Fly.io

## Recommended Configuration

### Dockerfile (Standalone Output)

Next.js standalone output creates a minimal production build:

```dockerfile
FROM node:20-alpine AS base

# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci

# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy standalone build
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

CMD ["node", "server.js"]
```

### next.config.js

Enable standalone output:

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig
```

### fly.toml

```toml
app = "my-nextjs-app"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[env]
  PORT = "3000"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true

  [http_service.concurrency]
    type = "requests"
    soft_limit = 200
    hard_limit = 250

[[http_service.checks]]
  grace_period = "30s"
  interval = "30s"
  method = "GET"
  path = "/"
  timeout = "10s"
```

## Common Issues

### Static Assets Not Loading

**Cause:** Static files not copied in Dockerfile.

**Fix:** Ensure this line is in Dockerfile:
```dockerfile
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
```

### API Routes Failing

**Cause:** Environment variables not set on Fly.io.

**Fix:**
```bash
fly secrets set DATABASE_URL="..." NEXTAUTH_SECRET="..."
```

### Image Optimization Errors

**Cause:** Sharp not installed for production image optimization.

**Fix:** Add to Dockerfile deps stage:
```dockerfile
RUN npm ci && npm install sharp
```

Or disable image optimization:
```js
// next.config.js
module.exports = {
  output: 'standalone',
  images: {
    unoptimized: true,
  },
}
```

### Build Too Slow

**Tips:**
1. Use `.dockerignore` to exclude node_modules, .next, .git
2. Leverage build cache with multi-stage builds
3. Use `npm ci` instead of `npm install`

**.dockerignore:**
```
node_modules
.next
.git
*.md
```

## Environment Variables

Next.js has build-time vs runtime env vars:

- `NEXT_PUBLIC_*` — Bundled at build time, exposed to browser
- Others — Available at runtime only (server-side)

For runtime secrets:
```bash
fly secrets set DATABASE_URL="..." API_KEY="..."
```

For build-time public vars:
```toml
[build.args]
  NEXT_PUBLIC_API_URL = "https://api.example.com"
```
docker.md
2.7 KB
# Docker Deployment on Fly.io

## Basic Deployment

If the project has a working Dockerfile:

```bash
fly launch --no-deploy
# Review fly.toml, then:
fly deploy
```

## Dockerfile Best Practices

### Multi-Stage Builds

Reduce image size with multi-stage builds:

```dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 8080
CMD ["node", "dist/index.js"]
```

### Non-Root User

Run as non-root for security:

```dockerfile
RUN addgroup --system --gid 1001 appgroup
RUN adduser --system --uid 1001 appuser
USER appuser
```

### Listen on 0.0.0.0

Apps must bind to all interfaces:

```dockerfile
# Ensure app listens on 0.0.0.0, not 127.0.0.1
ENV HOST=0.0.0.0
ENV PORT=8080
EXPOSE 8080
```

## fly.toml Configuration

```toml
app = "my-docker-app"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"
  # Or custom path:
  # dockerfile = "deploy/Dockerfile"

[http_service]
  internal_port = 8080  # Must match EXPOSE in Dockerfile
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true

[[http_service.checks]]
  grace_period = "15s"
  interval = "30s"
  method = "GET"
  path = "/health"
  timeout = "5s"
```

## Build Arguments

Pass build-time variables:

```toml
[build.args]
  NODE_ENV = "production"
  BUILD_VERSION = "1.0.0"
```

Use in Dockerfile:
```dockerfile
ARG NODE_ENV=development
ARG BUILD_VERSION
ENV NODE_ENV=$NODE_ENV
```

## .dockerignore

Always include to speed up builds:

```
.git
node_modules
*.md
.env*
.DS_Store
coverage
.next
dist
```

## Pre-Built Images

Deploy an existing image without building:

```toml
[build]
  image = "nginx:alpine"
```

Or from a registry:
```toml
[build]
  image = "ghcr.io/myorg/myapp:latest"
```

## Common Issues

### Port Mismatch

**Symptom:** Health checks fail, app seems to run.

**Fix:** Ensure `internal_port` matches `EXPOSE` and actual app port.

### Build Context Too Large

**Symptom:** Build takes forever to upload.

**Fix:** Add `.dockerignore` to exclude large/unnecessary files.

### Can't Find Entrypoint

**Symptom:** Container exits immediately.

**Fix:** Ensure Dockerfile has CMD or ENTRYPOINT:
```dockerfile
CMD ["node", "server.js"]
# Or
ENTRYPOINT ["./start.sh"]
```

### Secrets Not Available at Build Time

Build-time secrets use `--build-secret`:
```bash
fly deploy --build-secret MY_TOKEN=xxx
```

In Dockerfile:
```dockerfile
RUN --mount=type=secret,id=MY_TOKEN \
    cat /run/secrets/MY_TOKEN
```

Runtime secrets use `fly secrets set` (available as env vars).
nodejs.md
2.7 KB
# Node.js on Fly.io

## Recommended Dockerfile

```dockerfile
FROM node:20-alpine

WORKDIR /app

# Install dependencies first (cache layer)
COPY package*.json ./
RUN npm ci --only=production

# Copy application
COPY . .

# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nodeuser
USER nodeuser

ENV NODE_ENV=production
ENV PORT=8080
EXPOSE 8080

CMD ["node", "server.js"]
```

## fly.toml

```toml
app = "my-node-app"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[env]
  NODE_ENV = "production"
  PORT = "8080"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true

[[http_service.checks]]
  grace_period = "10s"
  interval = "30s"
  method = "GET"
  path = "/health"
  timeout = "5s"
```

## Express.js Example

```js
const express = require('express');
const app = express();

const PORT = process.env.PORT || 8080;
const HOST = '0.0.0.0';  // Important: bind to all interfaces

app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

app.get('/', (req, res) => {
  res.send('Hello from Fly.io!');
});

app.listen(PORT, HOST, () => {
  console.log(`Server running on ${HOST}:${PORT}`);
});
```

## Fastify Example

```js
const fastify = require('fastify')({ logger: true });

fastify.get('/health', async () => ({ status: 'ok' }));
fastify.get('/', async () => ({ hello: 'world' }));

const start = async () => {
  await fastify.listen({
    port: process.env.PORT || 8080,
    host: '0.0.0.0'  // Important
  });
};

start();
```

## TypeScript Projects

Dockerfile for TypeScript:

```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

USER node
EXPOSE 8080
CMD ["node", "dist/index.js"]
```

## .dockerignore

```
node_modules
.git
*.md
.env*
coverage
dist
.nyc_output
```

## Common Issues

### App Binds to localhost

**Symptom:** Works locally, fails health checks on Fly.

**Fix:** Bind to `0.0.0.0`:
```js
app.listen(PORT, '0.0.0.0');
```

### ES Modules

If using ES modules (`"type": "module"` in package.json):
```dockerfile
CMD ["node", "--experimental-specifier-resolution=node", "server.js"]
```

### Native Dependencies

Some packages need build tools:
```dockerfile
FROM node:20-alpine
RUN apk add --no-cache python3 make g++
# ... rest of Dockerfile
```

### Graceful Shutdown

Handle SIGTERM for clean shutdown:
```js
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down...');
  server.close(() => {
    process.exit(0);
  });
});
```
python.md
3.6 KB
# Python on Fly.io

## Flask

### Dockerfile

```dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY . .

# Create non-root user
RUN useradd --create-home appuser
USER appuser

ENV PORT=8080
EXPOSE 8080

# Use gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
```

### requirements.txt

```
flask
gunicorn
```

### app.py

```python
from flask import Flask
import os

app = Flask(__name__)

@app.route('/health')
def health():
    return 'OK', 200

@app.route('/')
def index():
    return 'Hello from Fly.io!'

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 8080))
    app.run(host='0.0.0.0', port=port)
```

## FastAPI

### Dockerfile

```dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd --create-home appuser
USER appuser

ENV PORT=8080
EXPOSE 8080

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
```

### requirements.txt

```
fastapi
uvicorn[standard]
```

### main.py

```python
from fastapi import FastAPI

app = FastAPI()

@app.get('/health')
async def health():
    return {'status': 'ok'}

@app.get('/')
async def root():
    return {'message': 'Hello from Fly.io!'}
```

## Django

### Dockerfile

```dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Collect static files
RUN python manage.py collectstatic --noinput

RUN useradd --create-home appuser
USER appuser

ENV PORT=8080
EXPOSE 8080

CMD ["gunicorn", "--bind", "0.0.0.0:8080", "myproject.wsgi:application"]
```

### fly.toml with migrations

```toml
app = "my-django-app"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[env]
  DJANGO_SETTINGS_MODULE = "myproject.settings"

[deploy]
  release_command = "python manage.py migrate"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
```

### Settings for Fly.io

```python
# settings.py
import os

ALLOWED_HOSTS = ['.fly.dev', 'localhost']

# Trust Fly.io proxy
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True

# Database from secret
DATABASES = {
    'default': dj_database_url.config(
        default=os.environ.get('DATABASE_URL')
    )
}
```

## fly.toml (General Python)

```toml
app = "my-python-app"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true

[[http_service.checks]]
  grace_period = "15s"
  interval = "30s"
  method = "GET"
  path = "/health"
  timeout = "5s"
```

## Common Issues

### gunicorn Workers

For better performance:
```dockerfile
CMD ["gunicorn", "--workers", "4", "--bind", "0.0.0.0:8080", "app:app"]
```

Or use gevent for async:
```dockerfile
CMD ["gunicorn", "--worker-class", "gevent", "--workers", "4", "--bind", "0.0.0.0:8080", "app:app"]
```

### Native Dependencies

Some packages need build tools:
```dockerfile
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*
```

### Poetry Projects

```dockerfile
FROM python:3.12-slim

WORKDIR /app

RUN pip install poetry
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false && \
    poetry install --no-dev --no-interaction

COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
```
static.md
3.0 KB
# Static Sites on Fly.io

## Using nginx

Best for pure HTML/CSS/JS sites.

### Dockerfile

```dockerfile
FROM nginx:alpine

# Copy static files
COPY . /usr/share/nginx/html

# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 8080
```

### nginx.conf

```nginx
events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    server {
        listen 8080;
        server_name _;
        root /usr/share/nginx/html;
        index index.html;
        
        # SPA fallback
        location / {
            try_files $uri $uri/ /index.html;
        }
        
        # Cache static assets
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
        
        # Health check
        location /health {
            access_log off;
            return 200 'OK';
            add_header Content-Type text/plain;
        }
    }
}
```

### fly.toml

```toml
app = "my-static-site"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true

[[http_service.checks]]
  grace_period = "5s"
  interval = "30s"
  method = "GET"
  path = "/health"
  timeout = "5s"
```

## SPA Frameworks (React, Vue, etc.)

For built SPA output (dist/build folder):

### Dockerfile

```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
```

## Alternative: Using Caddy

Simpler config, automatic HTTPS:

### Dockerfile

```dockerfile
FROM caddy:alpine

COPY Caddyfile /etc/caddy/Caddyfile
COPY . /srv

EXPOSE 8080
```

### Caddyfile

```
:8080 {
    root * /srv
    file_server
    
    # SPA fallback
    try_files {path} /index.html
    
    # Health check
    respond /health 200
}
```

## Minimal: Single HTML File

For very simple sites:

### Dockerfile

```dockerfile
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
EXPOSE 80
```

### fly.toml

```toml
app = "my-simple-site"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 80
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
```

## Common Issues

### 404 on Refresh (SPA)

**Cause:** Server returns 404 for client-side routes.

**Fix:** Add SPA fallback in nginx.conf:
```nginx
location / {
    try_files $uri $uri/ /index.html;
}
```

### Wrong MIME Types

**Symptom:** CSS/JS not loading, browser shows wrong content type.

**Fix:** Ensure nginx includes MIME types:
```nginx
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    ...
}
```

### Large Files

For sites with large assets, increase client_max_body_size:
```nginx
http {
    client_max_body_size 100M;
    ...
}
```
validate_fly_toml.py
5.7 KB
#!/usr/bin/env python3
"""
Validate fly.toml configuration before deployment.
Catches common issues that cause deployment failures.

Usage: python validate_fly_toml.py [path/to/fly.toml]
"""

import sys
import tomllib
from pathlib import Path


def load_toml(path: Path) -> dict:
    """Load and parse fly.toml."""
    try:
        with open(path, "rb") as f:
            return tomllib.load(f)
    except FileNotFoundError:
        return {"_error": f"File not found: {path}"}
    except tomllib.TOMLDecodeError as e:
        return {"_error": f"Invalid TOML syntax: {e}"}


def validate(config: dict) -> list[str]:
    """Validate fly.toml configuration. Returns list of issues."""
    issues = []
    
    if "_error" in config:
        return [config["_error"]]
    
    # Required: app name
    if "app" not in config:
        issues.append("Missing 'app' - app name is required")
    
    # Required: primary_region
    if "primary_region" not in config:
        issues.append("Missing 'primary_region' - deployment region is required")
    
    # Check build configuration
    build = config.get("build", {})
    has_dockerfile = "dockerfile" in build
    has_image = "image" in build
    has_builder = "builder" in build
    
    if not (has_dockerfile or has_image or has_builder):
        # Check if Dockerfile exists in current directory
        if not Path("Dockerfile").exists():
            issues.append(
                "No build configuration found. Add [build] section with "
                "'dockerfile', 'image', or 'builder', or create a Dockerfile"
            )
    
    # Check http_service or services
    http_service = config.get("http_service", {})
    services = config.get("services", [])
    
    if not http_service and not services:
        issues.append(
            "No service configuration found. Add [http_service] for HTTP apps "
            "or [[services]] for other protocols"
        )
    
    # Validate http_service
    if http_service:
        internal_port = http_service.get("internal_port")
        if internal_port is None:
            issues.append("[http_service] missing 'internal_port'")
        elif not isinstance(internal_port, int) or internal_port < 1 or internal_port > 65535:
            issues.append(f"[http_service] internal_port must be 1-65535, got: {internal_port}")
        
        # Check auto_stop/auto_start consistency
        auto_stop = http_service.get("auto_stop_machines")
        auto_start = http_service.get("auto_start_machines")
        
        if auto_stop and auto_stop != "off" and auto_start is False:
            issues.append(
                "Warning: auto_stop_machines is enabled but auto_start_machines is false. "
                "Machines may stop and not restart on traffic."
            )
        
        # Check health checks
        checks = http_service.get("checks", [])
        for i, check in enumerate(checks):
            if "path" not in check:
                issues.append(f"[http_service.checks[{i}]] missing 'path'")
            grace = check.get("grace_period", "0s")
            if grace == "0s":
                issues.append(
                    f"[http_service.checks[{i}]] grace_period is 0s - "
                    "app may fail health checks before it starts"
                )
    
    # Validate services sections
    for i, service in enumerate(services):
        if "internal_port" not in service:
            issues.append(f"[[services]][{i}] missing 'internal_port'")
        if "protocol" not in service:
            issues.append(f"[[services]][{i}] missing 'protocol' (tcp or udp)")
        
        ports = service.get("ports", [])
        if not ports:
            issues.append(f"[[services]][{i}] has no [[services.ports]] entries")
    
    # Check mounts for volume existence
    mounts = config.get("mounts")
    if mounts:
        source = mounts.get("source")
        if source:
            issues.append(
                f"Note: Using volume '{source}'. Ensure volume exists: "
                f"fly volumes create {source} --region <region>"
            )
    
    # Check deploy section
    deploy = config.get("deploy", {})
    release_cmd = deploy.get("release_command")
    if release_cmd:
        timeout = deploy.get("release_command_timeout", "5m")
        issues.append(
            f"Note: release_command configured ('{release_cmd}'). "
            f"Timeout is {timeout}. Increase if migrations are slow."
        )
    
    # Check env for common mistakes
    env = config.get("env", {})
    for key in env:
        if key.startswith("FLY_"):
            issues.append(
                f"[env] key '{key}' starts with FLY_ which is reserved for Fly.io runtime"
            )
        if "SECRET" in key.upper() or "PASSWORD" in key.upper() or "KEY" in key.upper():
            issues.append(
                f"Warning: [env] contains '{key}' which looks sensitive. "
                "Use 'fly secrets set' instead of [env] for secrets."
            )
    
    return issues


def main():
    path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("fly.toml")
    
    print(f"Validating: {path}")
    print("-" * 40)
    
    config = load_toml(path)
    issues = validate(config)
    
    if not issues:
        print("✓ No issues found")
        return 0
    
    errors = [i for i in issues if not i.startswith(("Note:", "Warning:"))]
    warnings = [i for i in issues if i.startswith("Warning:")]
    notes = [i for i in issues if i.startswith("Note:")]
    
    for issue in errors:
        print(f"✗ {issue}")
    for issue in warnings:
        print(f"⚠ {issue}")
    for issue in notes:
        print(f"ℹ {issue}")
    
    print("-" * 40)
    print(f"Errors: {len(errors)}, Warnings: {len(warnings)}, Notes: {len(notes)}")
    
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main())

Compatible Agents

Claude CodeclaudeCodexOpenClawAntigravityGemini

Details

Category
Uncategorized
Version
1.0.0
Stars
0
Added
January 27, 2026
Updated
February 5, 2026

Actions

Download .zip

Upload this .zip to Claude Desktop via Settings → Capabilities → Skills

Vote: