Deploying Python APIs to Production: Railway, Fly.io, and Render

Introduction

Heroku was the go-to platform for deploying Python web apps for over a decade. When it removed its free tier in 2022, developers started exploring alternatives. In 2026, three platforms have emerged as the clear favorites for deploying Python APIs: Railway, Fly.io, and Render.

Each has a different philosophy and different strengths. In this guide, we'll deploy the same FastAPI application to all three, compare the experience, and help you decide which platform fits your project.

🐳 Before deploying, you should have a working Dockerfile. See Dockerizing Django and FastAPI Applications: A Complete Guide.

Platform Overview: Railway, Fly.io, and Render — When to Choose Which

Here's the high-level comparison:

RailwayFly.ioRender
Best forFast deploys, ease of useGlobal low-latency, edgeTeams, preview environments
DeploymentGit push, Docker, CLIDockerfile (flyctl)Git push, Dockerfile
Free tier$5 credit/monthShared VMsLimited free services
DatabaseManaged PostgreSQL/Redis add-onsFly Postgres (managed)Managed PostgreSQL
RegionsMulti-region30+ global regionsMultiple regions
ScalingVertical + horizontalHorizontal (multiple VMs)Vertical + horizontal
Preview environmentsNoNoYes
Learning curveLowMediumLow

Choose Railway if

  • You want the simplest possible deploy experience
  • You're moving fast and want to go from code to production in under 5 minutes
  • You prefer a database as a first-class add-on within your project
  • You don't need global edge distribution

Choose Fly.io if

  • Low latency matters and your users are globally distributed
  • You need granular control over your infrastructure (memory, CPU, regions)
  • You're comfortable with a CLI-first workflow
  • You're running compute-intensive workloads (WebSockets, background workers)

Choose Render if

  • You work in a team and want preview environments for PRs
  • You want a Heroku-like experience with render.yaml as infrastructure-as-code
  • You want automatic deploys with zero-downtime on every push
  • You prefer a web dashboard over a CLI

Preparing Your Application for Production

Before deploying to any platform, ensure your application is production-ready.

Environment Variable Configuration

Never hardcode configuration. Use Pydantic Settings to load from environment variables:

# app/core/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache


class Settings(BaseSettings):
    # App
    APP_ENV: str = "development"
    DEBUG: bool = False
    SECRET_KEY: str

    # Database
    DATABASE_URL: str

    # Redis
    REDIS_URL: str = "redis://localhost:6379/0"

    # CORS
    ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]

    class Config:
        env_file = ".env"


@lru_cache()
def get_settings() -> Settings:
    return Settings()

Health Check Endpoint

All three platforms use health checks to verify your app is running. Add one:

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()


@app.get("/health")
async def health_check():
    return JSONResponse({"status": "healthy", "version": "1.0.0"})

Graceful Shutdown

Handle SIGTERM signals so in-flight requests complete before shutdown:

import signal
import asyncio

@app.on_event("shutdown")
async def shutdown_event():
    # Close database connections
    await database.disconnect()
    # Flush any pending jobs
    await redis_pool.aclose()

Production Dockerfile

FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# Install dependencies first (cache this layer)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

# Copy app
COPY . .

# Create non-root user
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s \
    CMD curl -f http://localhost:8000/health || exit 1

CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

Deploying a FastAPI App to Railway

Railway is the simplest of the three to get started with.

Step 1: Install the Railway CLI

# macOS
brew install railway

# or via npm
npm install -g @railway/cli

Step 2: Login and Initialize

railway login
railway init

Select "Empty Project" when prompted, then give your project a name.

Step 3: Add a PostgreSQL Database

# In your project directory
railway add --database postgres

Railway creates a managed PostgreSQL instance and automatically sets DATABASE_URL in your environment.

Step 4: Set Environment Variables

railway variables set SECRET_KEY="$(openssl rand -hex 32)"
railway variables set APP_ENV="production"
railway variables set DEBUG="false"

Step 5: Deploy

# Deploy from the current directory
railway up

Railway detects your Dockerfile automatically and builds + deploys it. The first deploy takes 2-3 minutes; subsequent deploys are faster.

Step 6: Get Your URL

railway domain
# Generates: https://your-app.up.railway.app

# Or set a custom domain
railway domain --add your-domain.com

FastAPI + Railway: Database Migrations

Run Alembic migrations as part of the deploy:

# In your Dockerfile, add a migration step
CMD ["sh", "-c", "uv run alembic upgrade head && uv run uvicorn app.main:app --host 0.0.0.0 --port 8000"]

Django on Railway

For Django, handle static files and ALLOWED_HOSTS:

# settings.py
import os

ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")

# Static files (use whitenoise for serving from Railway)
MIDDLEWARE = [
    "whitenoise.middleware.WhiteNoiseMiddleware",
    ...
]
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Add whitenoise
uv add whitenoise

# Set ALLOWED_HOSTS in Railway
railway variables set ALLOWED_HOSTS="your-app.up.railway.app,your-domain.com"

🔗 For the Django API itself: Building Your First API with Django: A Beginner's Guide

Deploying a Django App to Railway (Static Files and Migrations)

A complete Django + Railway Dockerfile:

FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

COPY . .

# Collect static files at build time
RUN uv run python manage.py collectstatic --noinput

RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

EXPOSE 8000

CMD ["sh", "-c", "uv run python manage.py migrate && uv run gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3"]

Deploying to Fly.io with flyctl

Fly.io takes a different approach: your app runs as a Firecracker microVM that can be placed in any of 30+ global regions.

Step 1: Install flyctl

# macOS
brew install flyctl

# Linux
curl -L https://fly.io/install.sh | sh

Step 2: Authenticate and Launch

fly auth login
fly launch

The fly launch command:

  1. Detects your Dockerfile
  2. Asks for app name and region
  3. Creates a fly.toml configuration file
  4. Optionally creates a Postgres database

The fly.toml Configuration File

# fly.toml
app = "my-fastapi-app"
primary_region = "lax"  # Los Angeles

[build]
  dockerfile = "Dockerfile"

[env]
  APP_ENV = "production"
  PORT = "8000"

[http_service]
  internal_port = 8000
  force_https = true
  auto_stop_machines = true   # stop idle machines to save cost
  auto_start_machines = true  # start on traffic
  min_machines_running = 1

  [http_service.concurrency]
    type = "requests"
    hard_limit = 200
    soft_limit = 150

[[vm]]
  memory = "512mb"
  cpu_kind = "shared"
  cpus = 1

[checks]
  [checks.health]
    port = 8000
    type = "http"
    path = "/health"
    interval = "15s"
    timeout = "5s"
    grace_period = "10s"

Step 3: Set Secrets

fly secrets set SECRET_KEY="$(openssl rand -hex 32)"
fly secrets set DATABASE_URL="postgresql://..."

Step 4: Deploy

fly deploy

Step 5: Scale to Multiple Regions

This is where Fly.io shines — you can deploy VMs in multiple regions with one command:

# Add a machine in Frankfurt for European users
fly machine run . --region fra

# List all machines
fly machine list

# Scale to 3 machines in the primary region
fly scale count 3

Fly.io Managed Postgres

# Create a Postgres cluster
fly postgres create --name my-db --region lax

# Attach it to your app (sets DATABASE_URL automatically)
fly postgres attach my-db

Run Database Migrations on Fly.io

# Run migrations without deploying
fly ssh console --command "uv run alembic upgrade head"

# Or use a release command in fly.toml
[deploy]
  release_command = "uv run alembic upgrade head"

Deploying to Render

Render is the closest modern equivalent to the classic Heroku experience.

  1. Go to render.com and create an account
  2. Click New → Web Service
  3. Connect your GitHub repository
  4. Render auto-detects your Dockerfile

Option 2: render.yaml (Infrastructure as Code)

Define your entire stack in a single file:

# render.yaml
services:
  - type: web
    name: fastapi-app
    runtime: docker
    plan: starter
    region: oregon
    healthCheckPath: /health
    envVars:
      - key: APP_ENV
        value: production
      - key: DATABASE_URL
        fromDatabase:
          name: mydb
          property: connectionString
      - key: SECRET_KEY
        generateValue: true

databases:
  - name: mydb
    plan: starter
    region: oregon
    databaseName: myapp
    user: myapp

Deploy with:

# Install the Render CLI
npm install -g @render-com/cli

# Deploy using render.yaml
render deploy

Preview Environments

Render's standout feature is preview environments — a new deployment for every pull request:

# render.yaml
previews:
  generation: automatic
  expireAfterDays: 7

When a PR is opened, Render:

  1. Builds a new Docker image
  2. Creates a new web service with a unique URL
  3. Creates a new database populated from your staging data (optionally)
  4. Posts the URL as a comment on the PR

This is invaluable for teams reviewing changes before merging.

Render Auto-Deploy

By default, Render deploys on every push to your main branch. You can configure this:

# render.yaml
services:
  - type: web
    name: fastapi-app
    autoDeploy: true         # deploy on every push
    branch: main             # only from main branch

Managing Environment Variables and Secrets Across Platforms

Best Practices

  1. Never commit secrets to Git — use .env for local development and platform-provided secret management for production
  2. Use per-environment configurationsDATABASE_URL should point to different databases in development, staging, and production
  3. Rotate secrets regularly — all three platforms support updating secrets without redeploying

Managing Secrets

# Railway
railway variables set KEY=value
railway variables list

# Fly.io
fly secrets set KEY=value
fly secrets list

# Render
# Use the dashboard (Settings → Environment) or render.yaml

Environment-Specific Settings

# app/core/config.py
class Settings(BaseSettings):
    APP_ENV: str = "development"

    @property
    def is_production(self) -> bool:
        return self.APP_ENV == "production"

    @property
    def database_pool_size(self) -> int:
        # More connections in production
        return 20 if self.is_production else 5

Database Management in Production

Connection Pooling

All three platforms provide managed PostgreSQL, but you should still configure connection pooling in your application:

# FastAPI with SQLAlchemy async
from sqlalchemy.ext.asyncio import create_async_engine

engine = create_async_engine(
    settings.DATABASE_URL,
    pool_size=10,
    max_overflow=20,
    pool_timeout=30,
    pool_recycle=1800,  # recycle connections every 30 minutes
    echo=settings.DEBUG,
)

🔗 See: Database Connection Pooling in FastAPI with SQLAlchemy

Running Migrations Safely

For zero-downtime migrations, follow these rules:

  1. Never remove a column that the running code still reads — add it, deploy, remove later
  2. Add nullable columns — don't add NOT NULL columns without a default
  3. Use Alembic with --sql mode to preview migration SQL before running
# Preview migration SQL
alembic upgrade head --sql

# Run with a timeout to prevent locking
alembic upgrade head  # your transaction timeout handles the rest

Database Backups

All three platforms offer automated backups for their managed databases:

# Railway: automatic daily backups in dashboard

# Fly.io: manual snapshot
fly postgres backup list
fly postgres backup create

# Render: automatic daily backups in dashboard

Custom Domains and SSL

Railway

railway domain --add api.yourdomain.com

Add a CNAME record in your DNS pointing to <your-app>.railway.app.

Fly.io

fly certs add api.yourdomain.com
fly certs show api.yourdomain.com  # get the DNS record to add

Fly.io provides automatic SSL via Let's Encrypt.

Render

In the dashboard: Settings → Custom Domains → Add Custom Domain. Render provides automatic SSL.

Scaling and Performance: Horizontal Scaling, Autoscaling, and Cold Starts

Horizontal Scaling

# Railway: scale via dashboard or CLI
railway up --replicas 3

# Fly.io
fly scale count 3            # 3 machines in primary region
fly scale count 2 --region fra  # 2 machines in Frankfurt

# Render: set via dashboard (Scaling → Number of Instances)

Autoscaling

Fly.io has the most sophisticated autoscaling:

# fly.toml
[http_service]
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1  # keep at least 1 always warm

Cold Starts

When auto_stop_machines = true (Fly.io) or a free tier service spins down (Render), the first request after idle time will experience a cold start — typically 1-5 seconds.

To minimize cold starts:

  • Set min_machines_running = 1 (Fly.io)
  • Upgrade to a paid plan with always-on instances (Render)
  • Use a health check pinger to keep instances warm (not recommended for production)

Cost Comparison: Railway vs. Fly.io vs. Render at Different Scales

Small Project (1 service + 1 database)

Platform~Monthly Cost
Railway$5–$20
Fly.io$0–$10 (if using auto-stop)
Render$7–$25

Medium Project (2 services + 1 database + Redis)

Platform~Monthly Cost
Railway$20–$60
Fly.io$20–$50
Render$40–$100

Notes on Costs

  • Railway bills by resource usage (CPU + RAM seconds) — predictable but can be expensive for always-on services
  • Fly.io has a generous free tier for small VMs; auto-stop significantly reduces costs
  • Render has a simpler pricing model (per-service per month) that's easier to budget

There's no single "best" platform — the right choice depends on your priorities:

Your situationRecommended platform
Solo developer, simple APIRailway (fastest to ship)
Global user baseFly.io (30+ regions)
Team with PR review workflowRender (preview environments)
Cost-sensitive hobby projectFly.io (auto-stop + free tier)
Heroku migrationRender (most similar UX)

Getting Started Checklist

Before deploying any Python API to production:

  • All config comes from environment variables (no hardcoded values)
  • /health endpoint returns 200
  • Dockerfile runs as non-root user
  • Database migrations are automated in the deploy process
  • uv.lock is committed to Git
  • Logging is configured to write to stdout (not files)
  • Connection pooling is configured appropriately

All three platforms are production-ready and actively maintained. Pick one, ship your app, and optimize later based on real traffic data.

🔗 Related: Setting Up CI/CD Pipelines with GitHub Actions for Django and FastAPI | Monitoring and Observability for Python APIs

STAY IN TOUCH

Get notified when I publish something new, and unsubscribe at any time.