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
  → PostgreSQL schema `pr_42` created (or dedicated database `pr_42`)
  → Redis database `1` assigned
  → HTTPS URL: https://pr-42.preview.myapp.com
  → Post-deploy: migrations run, seed data loaded, health check verified
  → PR merges → everything torn down automatically

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


Step 1: Dockerize your FastAPI app (if it isn't already)

A production-grade FastAPI Dockerfile in 2026:

# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim AS runner
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .

# FastAPI needs to know where the app object is
ENV MODULE_NAME=app.main
ENV VARIABLE_NAME=app
ENV PORT=3000

EXPOSE 3000

CMD ["sh", "-c", "uvicorn $MODULE_NAME:$VARIABLE_NAME --host 0.0.0.0 --port $PORT"]

Key points in this Dockerfile:

  • Multi-stage build minimizes image size by copying only runtime dependencies.
  • --host 0.0.0.0 is critical: FastAPI defaults to 127.0.0.1, which means the container won't accept external connections.
  • PORT=3000 follows the convention expected by most preview platforms. FastAPI's default is 8000; we override it here.
  • MODULE_NAME and VARIABLE_NAME as environment variables let you change the app entrypoint without modifying the Dockerfile.

If your FastAPI app uses async database drivers, you'll also need:

# Add to requirements.txt or install explicitly
RUN pip install asyncpg sqlalchemy[asyncio] aioredis

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 "→ Verifying health endpoint…"
sleep 2
curl -sf "http://localhost:${PORT:-3000}/health" || {
  echo "Health check failed!"
  exit 1
}

echo "✓ Preview environment ready: $PREVIEW_URL"

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 per-branch variables that don't leak secrets.

Create a .env.preview template:

# .env.preview
DATABASE_URL=postgresql+asyncpg://preview:preview@db:5432/preview
REDIS_URL=redis://cache:6379/0
SECRET_KEY=preview-only-not-for-production
PREVIEW_URL=${PREVIEW_URL}
API_V1_PREFIX=/api/v1
CORS_ORIGINS=["${PREVIEW_URL}", "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()

The PREVIEW_URL environment variable is injected at deploy time by the preview platform. Your app can use it to construct callback URLs, CORS origins, and OAuth redirect URIs.


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:

  • Dockerfile — builds and runs the app
  • bin/preview-entrypoint.sh — migrations, seed data, health check
  • .env.preview — per-branch 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 post-deploy command 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 3000

# ❌ Wrong — only for local development
uvicorn app.main:app --host 0.0.0.0 --port 3000 --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,  # Includes PREVIEW_URL
    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.

Ready to give every branch a live URL?

Free tier — 2 concurrent previews, no credit card required.

Start free