All posts
fastapipythonpreview-environmentsbackend

FastAPI Preview Environment: Stop Sharing One Staging Server Between 4 Engineers

PreviewDrop Team·May 20, 2026·10 min read

You're engineer #4 on a FastAPI backend. The staging server — a single DigitalOcean droplet lovingly named staging-do-not-touch — runs one instance of your app, one database, and one Redis. Three other engineers are also working on it today. Ali pushed a migration that renamed users.email to users.contact_email. Ben's feature branch references the old column. Chloe changed the Redis key format from cache:user:{id} to user:{id}:cache. Your branch — which touches none of these — is now broken because the shared database is in a state nobody intended.

Welcome to FastAPI's staging bottleneck. It's not a FastAPI problem — it's a "Python backends grow fast and the tooling hasn't caught up" problem. FastAPI's async performance, automatic OpenAPI docs, and Pydantic validation make it the fastest-growing Python framework in 2026. But the development workflow — particularly staging and review — is still stuck in 2018.

This guide shows you how to fix that. By the end, every branch in your FastAPI repo will get its own temporary, fully-isolated deployment with a real HTTPS URL — automatically, on every PR.


Why FastAPI specifically has this problem

FastAPI projects tend to grow faster than other Python frameworks. Here's why:

The "batteries included" productivity trap. FastAPI ships with automatic request validation (via Pydantic), automatic API documentation (via OpenAPI / Swagger UI), and dependency injection that makes testing straightforward. A junior developer can ship a working endpoint in their first week. This means FastAPI backends accumulate endpoints faster than Django or Flask projects — and with more endpoints comes more surface area for staging conflicts.

Async I/O means more shared resources. FastAPI's async handlers interact with databases (asyncpg, SQLAlchemy async), caches (aioredis), message queues (aio-pika for RabbitMQ), and external APIs (httpx). Each of these is a shared resource on a shared staging server. When two engineers modify the same resource — even in different branches — the staging server breaks for both of them.

The Docker lag. Django developers have been using Docker for staging environments since ~2017. Rails developers since 2018. FastAPI developers — many of whom come from data science or scripting backgrounds — are less likely to have Docker in their default toolchain. This means FastAPI staging environments are disproportionately bare-metal, single-instance setups with no isolation.


The solution: per-branch preview environments

The architecture:

GitHub PR #42
  → Docker image built from branch feature/new-auth
  → Container running on preview infrastructure
  → Database: a shared dev DB, or a per-branch schema/branch your startup script selects
  → HTTPS URL: https://pr-42.preview.myapp.com
  → On start: migrations run, seed data loaded, then the server starts
  → PR merges → everything torn down automatically

Here is how to set this up for a FastAPI project, step by step.


Step 1: Declare dependencies and bind to $PORT

You don't need to Dockerize anything for PreviewDrop. It detects FastAPI from requirements.txt or pyproject.toml and builds the image for you — a Dockerfile in the repo isn't used. What it does need:

  • Every dependency declared, including uvicorn and any async database drivers (asyncpg, sqlalchemy[asyncio], redis).
  • --host 0.0.0.0: FastAPI's server defaults to 127.0.0.1, which means the container won't accept external connections.
  • --port $PORT: PreviewDrop injects PORT; your app must listen on it.

If the detected start command doesn't match your app's layout, set the Start command in Project Settings:

uvicorn app.main:app --host 0.0.0.0 --port $PORT

For apps using SQLModel (which wraps SQLAlchemy and Pydantic), the dependency is:

# requirements.txt
fastapi==0.115.*
uvicorn[standard]==0.34.*
sqlmodel==0.0.*
asyncpg==0.30.*

Step 2: Create a preview entrypoint script

FastAPI apps often need setup before they can serve traffic: database migrations, seed data, cache warmup. Create a bin/preview-entrypoint.sh that runs these steps:

#!/usr/bin/env bash
# bin/preview-entrypoint.sh
set -euo pipefail

echo "→ Running database migrations…"
alembic upgrade head

echo "→ Loading seed data for preview…"
python -m app.seed_preview

echo "→ Starting the server…"
exec uvicorn app.main:app --host 0.0.0.0 --port "$PORT"

An example seed script for FastAPI + SQLModel:

# app/seed_preview.py
"""Seed the preview database with synthetic test data."""
import asyncio
from sqlmodel import Session, select
from app.database import engine
from app.models import User, Project

async def seed():
    async with Session(engine) as session:
        # Check if already seeded (idempotent)
        existing = await session.exec(select(User).limit(1))
        if existing.first():
            print("Database already seeded. Skipping.")
            return

        users = [
            User(email="alice@example.com", name="Alice Reviewer"),
            User(email="bob@example.com", name="Bob Dev"),
            User(email="carol@example.com", name="Carol PM"),
        ]
        session.add_all(users)
        await session.commit()
        print(f"Seeded {len(users)} users.")

if __name__ == "__main__":
    asyncio.run(seed())

Step 3: Set up environment variables

FastAPI uses environment variables (via os.getenv or Pydantic SettingsConfigDict) to configure database URLs, API keys, and feature flags. For preview environments, you need variables that don't leak secrets — PreviewDrop applies the project's variables to every preview.

Create a .env.preview template:

# .env.preview
DATABASE_URL=postgresql+asyncpg://preview:preview@your-preview-db.example.com:5432/preview
REDIS_URL=redis://your-redis.example.com:6379/0
SECRET_KEY=preview-only-not-for-production
API_V1_PREFIX=/api/v1
CORS_ORIGINS=["http://localhost:3000"]

For FastAPI, define these in a typed settings class:

# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/app"
    redis_url: str = "redis://localhost:6379/0"
    secret_key: str = "dev-secret"
    preview_url: str = "http://localhost:3000"
    cors_origins: list[str] = ["http://localhost:3000"]

    model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}

settings = Settings()

PreviewDrop does not inject the preview's public URL, and the database and Redis above are services you provide — a preview runs your app's container, not a database next to it. Preview hostnames follow prv-<id>.previews.previewdrop.dev, so for CORS pass allow_origin_regex=r"https://prv-[a-z0-9-]+\.previews\.previewdrop\.dev" to CORSMiddleware, and build callback or redirect URLs from the incoming request (request.base_url) instead of a fixed setting.


Step 4: Add a health check endpoint

Preview platforms (and load balancers) need a health check endpoint to verify the app is ready. FastAPI makes this trivial:

# app/main.py
from fastapi import FastAPI
from app.database import engine
from sqlmodel import text

app = FastAPI(title="MyApp", version="1.0.0")

@app.get("/health", tags=["system"])
async def health():
    """Health check for preview environments and load balancers."""
    # Verify database connectivity
    async with engine.begin() as conn:
        await conn.execute(text("SELECT 1"))
    return {"status": "ok", "database": "connected"}

This endpoint returns 200 when the app is healthy and 503 if the database is unreachable — which is exactly what container orchestrators and preview platforms check.


Step 5: Configure for preview deployment

At this point, your FastAPI repo has:

  • requirements.txt — every dependency, including uvicorn
  • bin/preview-entrypoint.sh — migrations, seed data, server start
  • .env.preview — preview environment variables
  • /health endpoint — platform health verification

Now connect your repo to PreviewDrop:

  1. Authenticate with GitHub
  2. Select your FastAPI repo
  3. Paste the contents of .env.preview into the environment variables section
  4. Set the Start command in Project Settings to: bash bin/preview-entrypoint.sh

The platform auto-detects FastAPI from requirements.txt or pyproject.toml and builds accordingly. The first build takes 2-4 minutes (installing Python dependencies, building the image). Subsequent builds are cached and take ~90 seconds.


Step 6: Verify the setup

Open a test PR. Within ~3 minutes, you get a GitHub comment with a URL:

🚀 Preview deployed: https://pr-42.previewdrop.dev

📚 API docs: https://pr-42.previewdrop.dev/docs
🔍 Health check: https://pr-42.previewdrop.dev/health

Click the /docs link. You get Swagger UI — automatically generated by FastAPI, running against the preview database, ready for interactive testing. Your PM can open this URL, click "Try it out" on any endpoint, and verify the API behavior without installing Python or pulling the branch.

This is the killer FastAPI feature: automatic OpenAPI docs on every preview URL. Reviewers can test the API interactively without any setup.


What you get that you didn't have before

  • Parallel API testing: Two PRs can be tested simultaneously with Swagger UI on different URLs. No more "who's using staging?"
  • Schema migration safety: Each preview runs its own alembic upgrade head. If a migration breaks, it breaks in the preview, not the shared staging database.
  • Frontend integration testing: Your React/Next.js frontend team can point their local dev server at https://pr-42.previewdrop.dev/api/v1 and test against your branch's API without deploying the backend themselves.
  • PM and QA self-service: Non-engineers can open the Swagger docs, enter test parameters, and verify behavior without touching a terminal.

FastAPI-specific gotchas to watch for

1. The uvicorn reload flag

In development, you run uvicorn --reload. In preview environments, never use --reload. It spawns a watchdog process that consumes extra memory and doesn't clean up properly on container stop.

# ✅ Correct for preview/production
uvicorn app.main:app --host 0.0.0.0 --port $PORT

# ❌ Wrong — only for local development
uvicorn app.main:app --host 0.0.0.0 --port $PORT --reload

2. Async database connection pools

FastAPI's async connection pool defaults to 20 connections. In a preview environment with a shared database server, 20 previews × 20 connections = 400 connections. That'll saturate most PostgreSQL instances.

Reduce the pool size for preview environments:

# app/database.py
import os
from sqlalchemy.ext.asyncio import create_async_engine

POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "5"))  # Smaller for previews

engine = create_async_engine(
    os.getenv("DATABASE_URL"),
    pool_size=POOL_SIZE,
    max_overflow=2,
)

3. CORS origins

Your FastAPI app needs to accept requests from the preview URL. If CORS is configured only for production URLs, the preview will reject frontend requests:

# app/main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.cors_origins,  # plus allow_origin_regex for preview hosts
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

4. Background tasks and lifespan events

If your FastAPI app uses lifespan events (@app.on_event("startup")) or BackgroundTasks, make sure they're compatible with ephemeral environments. A background task that expects to run for 10 minutes might get killed when the preview container is torn down on PR close.

For a deeper look at how branch preview environments work across frameworks, see our definitive guide to branch preview environments.


The FastAPI staging server is now per-branch

Your four engineers now have four isolated staging environments. Ali's migration renames users.email to users.contact_email — and it only affects Ali's preview. Ben's branch keeps working. Chloe's Redis key change is sandboxed. Your branch — the one that touches none of these — stays green.

No Slack messages. No staging lock. No "works on staging, broken on mine." Just four URLs, four databases, and four engineers shipping without stepping on each other.

Start setting up FastAPI branch previews → No credit card, automatic FastAPI detection, Swagger UI on every preview URL.

Want previews on your own pull requests? Start free — 2 concurrent previews, no credit card.