Disclaimer: This post is a personal, educational reference. All code examples and opinions are my own and are not affiliated with, sponsored by, or representative of my employer. I’m publishing this on my own time and without using any confidential information.
© 2026 Sean Miller. All rights reserved.
In modern app development, it’s common to start with a frontend-driven workflow (React, Vue, Svelte, etc.). It lets you iterate quickly: refine UX, pressure test flows, and nail down data schemas with mock data. But prototypes have a way of turning into products. Under pressure, you get piecemeal integrations (Auth, DB, payments), and before long the frontend is carrying far too much responsibility.
At some point you need a backend: a real server-side home for business logic, data access, and secrets.
The goal: build a FastAPI service, containerize it with Docker, and deploy it to Fly.io.
GET /health and POST /simulateIf you already have a backend and you just want the Fly deployment steps, skip down to Steps.
Start with a common scenario: an enterprise with three applications and no shared backend.
In this scenario, we have four teams that manage three separate applications: a user-facing web app, a user-facing mobile app, and an internal admin dashboard. Each team maintains its own business logic and integrations directly with the databases. This leads to duplicated effort, inconsistent authorization rules, and a security posture that’s hard to reason about.
The same enterprise with a shared backend looks different.
A backend is the server-side integration layer of an application. It’s the part that runs on a server and handles business logic, data access, and AuthN/AuthZ. It’s the layer your frontend talks to when it needs something durable and authorized to happen.
In practice, “backend” usually means:
Platforms like Supabase and Firebase can handle a lot before you need a traditional backend. But custom business logic, side effects, and strict control over secrets all demand a dedicated server-side layer.
If you’re brand new to backend work, this is the mindset shift: your backend is the contract between your UI and your data. Treat it as a product in its own right.
FastAPI is a modern, high-performance web framework for building APIs with Python. It’s built on top of Starlette (ASGI) and Pydantic (validation + serialization).
FastAPI’s killer feature is that you can write Python functions with type hints and get:
without writing a bunch of glue code.
Fly.io is a platform for deploying and running your apps as containers on lightweight VMs (“machines”), close to your users. It’s a strong fit for small-to-medium APIs because the workflow is simple:
Fly runs real processes in containers. It is not a serverless function platform. The two most common deployment bugs are binding to 127.0.0.1 instead of 0.0.0.0, and listening on the wrong port.
Several alternatives exist: serverless functions (AWS Lambda), serverless containers (Cloud Run), and edge runtimes (Cloudflare Workers). The trade space at a glance:
| Platform | Type | Pros | Cons |
|---|---|---|---|
| AWS Lambda | Serverless Function | Scales to zero; huge ecosystem; fine-grained IAM | Cold starts; packaging Python deps can be annoying; local dev can be awkward |
| Google Cloud Run | Serverless Container | ”Just a container”; scales well; straightforward HTTP services | More cloud surface area (GCP); IAM/networking complexity if you’re new |
| Supabase Edge Functions | Edge Function | Tight Supabase integration; low ops overhead | Edge runtime constraints; not ideal for heavy Python backends |
| Vercel Edge Functions | Edge Function | Great if you live in Vercel; very fast at the edge | Not a general-purpose backend; runtime limits; best for light logic |
| Cloudflare Workers | Edge Function | Extremely fast global edge; generous free tier | Different runtime model; not a drop-in for typical Python APIs |
The example creates two endpoints: GET /health and POST /simulate (deterministic calculations based on input parameters).
The examples below use the fly CLI. Some setups still use flyctl. Same tool.
mkdir fastapi-fly-example # create new directory
cd fastapi-fly-example # change to directory
python3 -m venv .venv # create virtual environment
source .venv/bin/activate # activate virtual environment
python -m pip install --upgrade pip
Install dependencies:
pip install fastapi "uvicorn[standard]"
Create a minimal main.py:
from __future__ import annotations
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(title="FastAPI on Fly.io", version="0.1.0")
@app.get("/health")
def health():
return {"status": "ok"}
class SimulateRequest(BaseModel):
seed: int = Field(..., description="Starting seed for a deterministic simulation")
steps: int = Field(10, ge=1, le=100_000)
@app.post("/simulate")
def simulate(body: SimulateRequest):
# Simple deterministic PRNG (linear congruential generator)
value = body.seed & 0x7FFFFFFF
for _ in range(body.steps):
value = (1103515245 * value + 12345) & 0x7FFFFFFF
return {"seed": body.seed, "steps": body.steps, "result": value}
Create a requirements.txt (pin versions later if you want reproducibility):
fastapi
uvicorn[standard]
Start the server:
uvicorn main:app --reload --host 127.0.0.1 --port 8000
Test it:
curl -s http://127.0.0.1:8000/health
curl -s -X POST http://127.0.0.1:8000/simulate \
-H "Content-Type: application/json" \
-d '{"seed":42,"steps":1000}'
FastAPI also gives you API docs out of the box:
http://127.0.0.1:8000/docshttp://127.0.0.1:8000/openapi.jsonCreate a Dockerfile:
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
Create a .dockerignore:
.venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.DS_Store
Build + run the container:
docker build -t fastapi-fly-example .
docker run --rm -p 8080:8080 fastapi-fly-example
In another terminal:
curl -s http://127.0.0.1:8080/health
Authenticate:
fly auth login
Initialize the app (this creates fly.toml):
fly launch --no-deploy
Open fly.toml and confirm the internal port matches your Docker command above (8080). Depending on your CLI version, this may be under [http_service] or [[services]].
# fly.toml (relevant part)
[http_service]
internal_port = 8080
force_https = true
fly deploy
Then open the app:
fly open
Grab your app URL (from fly open or fly status) and test:
curl -s https://YOUR-APP.fly.dev/health
curl -s -X POST https://YOUR-APP.fly.dev/simulate \
-H "Content-Type: application/json" \
-d '{"seed":42,"steps":1000}'
Useful commands:
fly status
fly logs
fly ssh console
fly secrets set MY_API_KEY="..."
If your deploy succeeds but you get 502 or health check failures, it’s almost always one of these:
127.0.0.1 instead of 0.0.0.0 inside the containerinternal_portFor FastAPI + Uvicorn on Fly, the safe default is:
--host 0.0.0.0--port 8080 (and set Fly internal_port = 8080)Once the deploy loop is solid, the next moves are:
Writing endpoints is the easy part. Building a backend you can deploy, observe, and trust is the real work. Fly + FastAPI is a solid starting point.