Deploy FastAPI with AZIN

All guides
FastAPI
Deploy Guides·1 min read

Deploy FastAPI with AZIN

deployfastapipythonpostgresql

FastAPI production deployment means configuring an ASGI server, managing async database connections, handling environment variables, and keeping workers alive under load. AZIN handles all of it: Railpack auto-detects FastAPI, configures Uvicorn, provisions PostgreSQL and Redis in your GCP account, and deploys on every git push. No Nginx. No Dockerfile. Push code, it's live.

#How AZIN detects FastAPI

AZIN uses Railpack to auto-detect your project type. When it finds a Python dependency file with fastapi in your dependencies, it identifies a FastAPI application and configures an ASGI-appropriate build.

Detection files Railpack looks for:

  • requirements.txt
  • pyproject.toml
  • Pipfile
  • setup.py

Once detected, Railpack resolves your Python version from .python-version first, then runtime.txt, and defaults to Python 3.13 if neither is present. Package manager detection is automatic — pip, Poetry, and Pipenv are all supported.

What Railpack configures automatically for FastAPI:

  • Python version from .python-version or runtime.txt (default: 3.13)
  • Dependencies installed via pip, Poetry, or Pipenv based on your lockfile
  • Uvicorn as the ASGI server — not Gunicorn, because FastAPI is an ASGI framework
  • Start command set to uvicorn main:app --host 0.0.0.0 --port $PORT

No Procfile, no Dockerfile, no manual server configuration. FastAPI is ASGI-native, so Railpack uses Uvicorn directly rather than wrapping it behind a WSGI server. If you already have a Dockerfile, AZIN supports that too — but for most FastAPI apps, Railpack handles everything.

#Deployment config

Connect your GitHub repository to AZIN and it deploys on every push. For a FastAPI app with PostgreSQL and Redis:

name: my-fastapi-app
cloud: gcp
region: us-central1
services:
  api:
    build:
      type: railpack
    env:
      ENVIRONMENT: production
      PORT: "8000"
    scaling:
      min: 1
      max: 10
      target_cpu: 70
  db:
    type: postgres
    plan: production
  cache:
    type: redis

AZIN injects DATABASE_URL and REDIS_URL into your API service automatically. The db service provisions Cloud SQL in your GCP account. The cache service provisions Memorystore. Both live in your cloud, not on shared infrastructure.

API with background worker

For an API with a background worker processing tasks from a Redis queue:

name: my-fastapi-app
cloud: gcp
region: us-central1
services:
  api:
    build:
      type: railpack
    env:
      ENVIRONMENT: production
      PORT: "8000"
    scaling:
      min: 1
      max: 10
      target_cpu: 70
  worker:
    build:
      type: railpack
    start: arq app.worker.WorkerSettings
    env:
      ENVIRONMENT: production
  db:
    type: postgres
    plan: production
  cache:
    type: redis

Both services share the same codebase, the same DATABASE_URL, and the same REDIS_URL. The worker service overrides the start command to run your task processor instead of Uvicorn.

#FastAPI app configuration for production

FastAPI uses Pydantic for settings management. Define a Settings class that reads from environment variables — this keeps secrets out of your codebase and lets AZIN inject values at runtime:

# app/config.py
from pydantic_settings import BaseSettings
 
class Settings(BaseSettings):
    environment: str = "development"
    database_url: str
    redis_url: str = ""
    secret_key: str
    allowed_origins: list[str] = ["https://yourdomain.com"]
 
    model_config = {"env_file": ".env"}
 
settings = Settings()

Your main application file wires up the settings and configures FastAPI for production:

# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from app.config import settings
from app.database import engine
 
@asynccontextmanager
async def lifespan(app: FastAPI):
    async with engine.begin() as conn:
        await conn.execute(text("SELECT 1"))
    yield
    await engine.dispose()
 
app = FastAPI(
    title="My API",
    docs_url="/docs" if settings.environment != "production" else None,
    redoc_url=None,
    lifespan=lifespan,
)
 
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_methods=["*"],
    allow_headers=["*"],
)

Key points:

  • pydantic-settings reads DATABASE_URL and REDIS_URL from the environment — both injected by AZIN automatically
  • The lifespan context manager replaces the deprecated on_event decorators and handles startup/shutdown cleanly
  • Disable /docs in production to avoid exposing your API schema publicly
  • CORS origins should be restricted to your actual frontend domains

Your requirements.txt for a FastAPI app with PostgreSQL and Redis:

fastapi>=0.115
uvicorn[standard]>=0.32
pydantic-settings>=2.6
sqlalchemy[asyncio]>=2.0
asyncpg>=0.30
redis>=5.0
alembic>=1.14

uvicorn[standard] includes uvloop and httptools — both provide meaningful performance improvements over the pure-Python defaults.

Info

Never use uvicorn main:app --reload in production. The --reload flag watches for file changes and restarts the server — useful for development, wasteful and unstable in production. Railpack configures Uvicorn without --reload automatically.

#Async database access

FastAPI is async-native, so your database layer should be too. SQLAlchemy 2.0+ with asyncpg gives you a fully async PostgreSQL connection pool:

# app/database.py
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from app.config import settings
 
# Convert postgres:// to postgresql+asyncpg://
database_url = settings.database_url.replace(
    "postgres://", "postgresql+asyncpg://"
)
 
engine = create_async_engine(
    database_url,
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,
    pool_recycle=300,
)
 
async_session = async_sessionmaker(engine, class_=AsyncSession)
 
async def get_db():
    async with async_session() as session:
        yield session

Use the session in your route handlers via FastAPI's dependency injection:

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import User
 
router = APIRouter()
 
@router.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404)
    return user

pool_pre_ping prevents stale connection errors after Cloud SQL restarts. pool_recycle=300 closes connections older than 5 minutes — Cloud SQL's proxy idle timeout is 10 minutes, so recycling at 5 keeps you well within bounds.

For migrations, use Alembic with async support. Add a release_command to your azin.yaml to run migrations before the new version receives traffic:

services:
  api:
    build:
      type: railpack
    deploy:
      release_command: "alembic upgrade head"
    env:
      ENVIRONMENT: production
      PORT: "8000"

If the migration fails, the deploy is cancelled and the previous version continues serving requests.

#Background tasks and workers

FastAPI offers three approaches to background work, depending on the task duration and reliability requirements.

For short, fire-and-forget tasks — use FastAPI's built-in BackgroundTasks. These run in the same process as your API after the response is sent:

from fastapi import BackgroundTasks
 
async def send_welcome_email(email: str):
    ...
 
@router.post("/users")
async def create_user(
    user: UserCreate,
    background_tasks: BackgroundTasks,
    db: AsyncSession = Depends(get_db),
):
    new_user = User(**user.model_dump())
    db.add(new_user)
    await db.commit()
    background_tasks.add_task(send_welcome_email, new_user.email)
    return new_user

This works for lightweight tasks like sending emails or logging events. The task dies if the pod restarts — there's no retry mechanism.

For durable, retryable tasks — use a dedicated worker service with ARQ (async Redis queue). ARQ is the async-native option and pairs well with FastAPI:

# app/worker.py
from arq import cron
from arq.connections import RedisSettings
from app.config import settings
 
async def process_payment(ctx, payment_id: int):
    ...
 
async def daily_report(ctx):
    ...
 
class WorkerSettings:
    functions = [process_payment]
    cron_jobs = [cron(daily_report, hour=6, minute=0)]
    redis_settings = RedisSettings.from_dsn(settings.redis_url)
    max_jobs = 10
    job_timeout = 300

Enqueue jobs from your API routes:

from arq import create_pool
from arq.connections import RedisSettings
from app.config import settings
 
@router.post("/payments")
async def create_payment(payment: PaymentCreate, db: AsyncSession = Depends(get_db)):
    ...
    redis = await create_pool(RedisSettings.from_dsn(settings.redis_url))
    await redis.enqueue_job("process_payment", payment.id)
    return {"status": "processing"}

The worker service in your azin.yaml runs arq app.worker.WorkerSettings as a separate process. It shares REDIS_URL with your API service, so jobs flow through Memorystore in your GCP account.

#Why AZIN for FastAPI hosting

Your cloud, your data. FastAPI apps and PostgreSQL databases run in your own GCP account. You own the infrastructure, the billing relationship with Google, and the data. AZIN is the control plane — not the cloud provider. AWS and Azure are on our roadmap.

Native async support. FastAPI is ASGI-native, and Railpack configures Uvicorn automatically — no WSGI/Gunicorn mismatch. Other platforms default to Gunicorn for all Python apps, forcing you to manually configure Uvicorn workers. AZIN detects FastAPI and uses the right server out of the box.

Managed PostgreSQL and Redis. Cloud SQL and Memorystore are provisioned in your GCP account. Automated backups, connection pooling, and encryption at rest included. DATABASE_URL and REDIS_URL are injected at runtime — your FastAPI app reads them from the environment with no manual setup.

No cold starts for production traffic. GKE Autopilot keeps pods warm when min: 1 is set. The first GKE cluster is free — you pay only for the pods running your application. This differs from platforms where a managed Kubernetes cluster can cost ~$225/month in underlying cloud fees before any workloads run (based on typical AWS EKS configurations, as of February 2026).

Scale-to-zero staging. Deploy staging environments on lttle.cloud (in early access). When your staging FastAPI app receives no traffic, it scales to zero and costs nothing. Production stays warm on GKE; staging idles without burning compute.

Deploy Guides

Deploy Flask with AZIN

Flask's synchronous WSGI alternative. Same zero-config detection, managed Postgres, GCP BYOC.

#Frequently asked questions

Deploy on private infrastructure

Managed AI environments with built-in isolation. Zero DevOps required.