Skip to main content
fastapideploymentrailwayrenderdockerproduction

Deploying FastAPI to Railway and Render: A Production Guide

Step-by-step production deployment of a FastAPI app with Postgres, Redis, and Celery workers on Railway and Render — including migration automation and health checks.

FastAPI AI Kit Team··3 min read

Getting FastAPI running locally is easy. Getting it deployed correctly — with database migrations running automatically, Celery workers scaling independently, health checks, and environment variable management — is where most guides stop. This covers both Railway and Render deployments fully.

What a production FastAPI deployment needs

Before diving in, the checklist:

  • Dockerfile — multi-stage build, non-root user, pinned dependencies
  • Health endpoint/healthz that checks DB and Redis connectivity
  • Migration on startupalembic upgrade head before the server starts
  • Worker separation — Celery workers as a separate service
  • Environment variables — no secrets in the image, everything via env
  • Logging — structured JSON logs, not print statements

Dockerfile

# Multi-stage build
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim AS runtime
WORKDIR /app

# Non-root user for security
RUN useradd --system --uid 1001 app

COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .

RUN chown -R app:app /app
USER app

EXPOSE 8000
CMD ["sh", "-c", "alembic upgrade head && gunicorn app.main:app -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 2"]

The startup command runs alembic upgrade head before the server starts — every deploy automatically applies pending migrations.

Health endpoint

@router.get("/healthz")
async def health(
    db: AsyncSession = Depends(get_db),
):
    try:
        await db.execute(text("SELECT 1"))
        db_ok = True
    except Exception:
        db_ok = False
    
    try:
        await redis_client.ping()
        redis_ok = True
    except Exception:
        redis_ok = False
    
    status = "healthy" if (db_ok and redis_ok) else "degraded"
    code = 200 if status == "healthy" else 503
    
    return JSONResponse(
        status_code=code,
        content={"status": status, "db": db_ok, "redis": redis_ok},
    )

Railway deployment

Railway detects your Dockerfile automatically. The workflow:

# Install Railway CLI
npm install -g @railway/cli

# Login and link project
railway login
railway init

# Add Postgres and Redis from dashboard
# Then set environment variables:
railway env set \
    DATABASE_URL="$RAILWAY_POSTGRES_URL" \
    REDIS_URL="$RAILWAY_REDIS_URL" \
    OPENAI_API_KEY="sk-..." \
    JWT_SECRET_KEY="$(openssl rand -hex 32)" \
    STRIPE_SECRET_KEY="sk_live_..."

# Deploy
railway up

For Celery workers, create a second Railway service pointing to the same repo with a different start command:

# Worker service start command
celery -A app.worker worker --loglevel=info --concurrency=2

Render deployment via render.yaml

Render uses a render.yaml blueprint for infrastructure-as-code:

# render.yaml
services:
  - type: web
    name: fastapikit-api
    env: docker
    dockerfilePath: Dockerfile
    envVars:
      - fromGroup: fastapikit-secrets
      - key: DATABASE_URL
        fromDatabase:
          name: fastapikit-db
          property: connectionString
      - key: REDIS_URL
        fromService:
          name: fastapikit-redis
          property: connectionString
    healthCheckPath: /healthz
    
  - type: worker
    name: fastapikit-worker
    env: docker
    dockerCommand: celery -A app.worker worker --loglevel=info
    envVars:
      - fromGroup: fastapikit-secrets

databases:
  - name: fastapikit-db
    databaseName: fastapikit
    plan: starter

Create a fastapikit-secrets env group in Render dashboard with OPENAI_API_KEY, JWT_SECRET_KEY, and STRIPE_SECRET_KEY. Everything else is wired automatically by the blueprint.

Environment variable management

Never commit secrets. Use a .env.example with placeholder values:

# .env.example
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/dbname
REDIS_URL=redis://localhost:6379
OPENAI_API_KEY=sk-your-key-here
ANTHROPIC_API_KEY=sk-ant-your-key-here
JWT_SECRET_KEY=generate-with-openssl-rand-hex-32
STRIPE_SECRET_KEY=sk_live_or_test_key
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret

Structured logging

Don't use print() in production. Use structured JSON logging:

import structlog

logger = structlog.get_logger()

# In your route handlers:
logger.info(
    "chat_request",
    api_key_id=str(key.id),
    model=body.model,
    tokens=response.tokens,
    latency_ms=elapsed,
)

Railway and Render both pick up structured logs automatically in their log viewers.

FastAPI AI Kit ships all of this: the Dockerfile, healthz endpoint, alembic startup, render.yaml blueprint, Railway documentation, and structured logging configuration.

Build your AI backend with FastAPI AI Kit.

Clone, configure, and ship — everything is already wired up.

Read the docs
No subscriptions · One-time payment · Lifetime updates