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-deployFiles
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