Overview
Fly.io runs your container as Machines near your users. This guide takes a FastAPI app from a Dockerfile to a deployed service with a database and secrets. Fly’s Managed Postgres is a paid product with no free tier (at the time of writing, the Basic plan starts at $38 per month), so decide on the database before attaching one. App patterns: fastapi.
Prerequisites
flyctlinstalled and signed in:brew install flyctlorcurl -L https://fly.io/install.sh | sh, thenfly auth login.- A FastAPI app with a health route such as
/healthz. - Docker, optionally, to test the image locally.
Steps
1. Write the Dockerfile
fly launch can generate one for Python apps, but an explicit file keeps the image reviewable.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]Bind 0.0.0.0, not 127.0.0.1; Fly’s proxy cannot reach a loopback-only server. Test with docker build -t myapi . && docker run -p 8080:8080 myapi, then curl localhost:8080/healthz.
2. Launch without deploying
fly launch --no-deployThis writes fly.toml. Decline any database prompt; step 4 attaches one explicitly.
3. Check fly.toml
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[vm]]
memory = "512mb"
cpu_kind = "shared"
cpus = 1internal_port must match the port in CMD. With auto_stop_machines = "stop", Fly stops idle Machines and starts them on the next request, which saves money but adds a cold start. Set min_machines_running = 1 (it applies to the primary region) when cold starts are unacceptable. Enable or disable auto_stop_machines and auto_start_machines together, or a Machine may never start or never stop. Raise memory if the app loads large models.
4. Create and attach Managed Postgres
fly mpg create --name myapi-db --region iad
fly mpg attach <cluster-id> --app myapifly mpg create also takes --plan, --volume-size, and --pg-major-version. Attaching sets a DATABASE_URL secret on the app that points at the pooled endpoint (PgBouncer). This guide does not cover the older unmanaged fly postgres clusters.
import os
DATABASE_URL = os.environ["DATABASE_URL"]5. Set secrets and deploy
fly secrets set SECRET_KEY="$(openssl rand -hex 32)" --app myapi
fly deploy --app myapiSecrets are encrypted and exposed as environment variables; never put them in fly.toml or the Dockerfile. Setting a secret restarts the app’s Machines.
6. Scale if needed
fly scale count 2 --app myapi # two MachinesVerify it worked
fly status --app myapi
curl -sI https://myapi.fly.dev/healthz | head -1 # HTTP/2 200
fly secrets list --app myapi # lists DATABASE_URL and SECRET_KEY by nameCommon errors
- The container exits immediately, or the proxy reports no healthy upstream. The server binds loopback, or
internal_portdiffers from the listening port. DATABASE_URLis missing. The attach step was skipped or used the wrong cluster ID;fly mpg listshows IDs.- “No Dockerfile found”. Run the command from the directory that holds it.
- High latency on first request after idle. Autostop woke a stopped Machine; set
min_machines_running = 1. - Out-of-memory kills. Raise
memoryunder[[vm]];fly logsshows OOM messages.
FAQ
Does Fly.io have a free Postgres tier?
No. Fly’s Managed Postgres is a paid product with no free tier; at the time of writing, the Basic plan starts at $38 per month.
Why does Fly report no healthy upstream for my FastAPI app?
The server binds 127.0.0.1 or internal_port differs from the listening port. Bind 0.0.0.0 and make internal_port match the port in CMD.
How do I avoid cold starts on Fly.io?
Set min_machines_running = 1, which applies to the primary region; with auto_stop_machines = "stop", idle Machines stop and the next request pays a cold start.
Where should I put secrets for a Fly.io app?
Use fly secrets set, which stores them encrypted and exposes them as environment variables. Never put secrets in fly.toml or the Dockerfile, and expect a restart when you set one.