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.
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 —
/healthzthat checks DB and Redis connectivity - Migration on startup —
alembic upgrade headbefore 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.
