Skip to content

Deploying to Fly.io

Fly.io is a modern platform for running containerised apps globally. It provisions hardware, handles TLS/HTTPS, and auto-scales your instances.

This project ships with a multi-stage Dockerfile that builds the MkDocs documentation site and embeds it directly into the container. After deployment, the documentation is served at the root URL and the API is available under /api.


URL Map (Post-Deployment)

Path What is served
/ MkDocs documentation home page
/getting-started/ Documentation sub-pages
/api/docs Swagger / OpenAPI interactive UI
/api/redoc ReDoc alternative API reference
/api/openapi.json Raw OpenAPI schema
/api/v1/chat/ask Primary RAG endpoint
/api/v1/training/data Training management
/health Liveness probe (used by Fly.io)

Prerequisites

  • A Fly.io account (free tier available)
  • The Fly CLI (flyctl) installed

1. Install the Fly CLI

pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"
brew install flyctl
curl -L https://fly.io/install.sh | sh

Verify:

fly version

2. Authenticate

fly auth login

3. Review fly.toml

The project ships with a production-ready fly.toml:

fly.toml
app = "dbn-analytics-poc"
primary_region = "iad"  # US East

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https   = true
  auto_stop_machines  = true
  auto_start_machines = true
  min_machines_running = 0

  [[http_service.checks]]
    path     = "/health"
    interval = "30s"
    timeout  = "5s"

[env]
  APP_NAME    = "DBN Analytics POC API"
  VANNA_MODEL = "gpt-4o"

[[vm]]
  cpu_kind  = "shared"
  cpus      = 1
  memory_mb = 1024

Change region

Run fly platform regions to see all options. Replace iad with lhr (London) or fra (Frankfurt) for lower latency from Nigeria/Africa.


4. Provision the App (No Deploy Yet)

fly launch --no-deploy

If the app name dbn-analytics-poc is already taken, Fly will prompt for a new name. Update the app field in fly.toml to match.


5. Set Secrets

Since .env is excluded by .dockerignore, inject secrets directly:

fly secrets set OPENAI_API_KEY="sk-proj-..."

Verify:

fly secrets list

6. Deploy

fly deploy

The deploy process:

  1. Sends the build context to Fly's remote builder
  2. Runs the docs-builder stage — mkdocs build generates the site/ folder
  3. Runs the production stage — copies site/ into the final image alongside the FastAPI app
  4. Starts the container with uvicorn main:app --host 0.0.0.0 --port 8080

7. Visit Your App

fly open

This opens https://dbn-analytics-poc.fly.dev in your browser — which displays the MkDocs documentation home page served by FastAPI.

URL Content
https://dbn-analytics-poc.fly.dev/ 📖 Documentation home
https://dbn-analytics-poc.fly.dev/api/docs ⚡ Swagger UI
https://dbn-analytics-poc.fly.dev/health ✅ Health probe

8. Re-Deploy After Changes

After any code or documentation change, simply run:

fly deploy

The multi-stage build automatically rebuilds the docs and redeploys.


Useful Commands

Command Description
fly status View machine and release status
fly logs Stream live application logs
fly ssh console SSH into the running container
fly secrets set KEY=VALUE Add or update a secret
fly secrets list List all secret names (not values)
fly scale memory 2048 Increase RAM if needed
fly open Open the live app in your browser

How the Static File Serving Works

The Dockerfile uses a multi-stage build:

Dockerfile (simplified)
# Stage 1: Build MkDocs site
FROM python:3.10-slim AS docs-builder
RUN pip install mkdocs-material
COPY mkdocs.yml .
COPY docs/ ./docs/
RUN mkdocs build --site-dir site   #  generates site/

# Stage 2: Production image
FROM python:3.10-slim AS production
COPY --from=docs-builder /app/site ./site   # embed docs
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", ...]

main.py detects the site/ directory at startup and mounts it:

main.py (serving logic)
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse

if os.path.isdir("site"):
    # Serve docs home at /
    @app.get("/", include_in_schema=False)
    def serve_docs_home():
        return FileResponse("site/index.html")

    # Catch-all: serve any MkDocs-generated sub-page
    @app.get("/{full_path:path}", include_in_schema=False)
    def serve_docs_page(full_path: str):
        ...  # resolves to site/<path>/index.html or falls back to site/index.html