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:
| Railway | Fly.io | Render | |
|---|---|---|---|
| Best for | Fast deploys, ease of use | Global low-latency, edge | Teams, preview environments |
| Deployment | Git push, Docker, CLI | Dockerfile (flyctl) | Git push, Dockerfile |
| Free tier | $5 credit/month | Shared VMs | Limited free services |
| Database | Managed PostgreSQL/Redis add-ons | Fly Postgres (managed) | Managed PostgreSQL |
| Regions | Multi-region | 30+ global regions | Multiple regions |
| Scaling | Vertical + horizontal | Horizontal (multiple VMs) | Vertical + horizontal |
| Preview environments | No | No | Yes |
| Learning curve | Low | Medium | Low |
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.yamlas 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:
- Detects your Dockerfile
- Asks for app name and region
- Creates a
fly.tomlconfiguration file - 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.
Option 1: Deploy via GitHub (Recommended)
- Go to render.com and create an account
- Click New → Web Service
- Connect your GitHub repository
- 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:
- Builds a new Docker image
- Creates a new web service with a unique URL
- Creates a new database populated from your staging data (optionally)
- 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
- Never commit secrets to Git — use
.envfor local development and platform-provided secret management for production - Use per-environment configurations —
DATABASE_URLshould point to different databases in development, staging, and production - 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:
- Never remove a column that the running code still reads — add it, deploy, remove later
- Add nullable columns — don't add NOT NULL columns without a default
- 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
Conclusion: Recommended Setup for Indie Developers and Small Teams
There's no single "best" platform — the right choice depends on your priorities:
| Your situation | Recommended platform |
|---|---|
| Solo developer, simple API | Railway (fastest to ship) |
| Global user base | Fly.io (30+ regions) |
| Team with PR review workflow | Render (preview environments) |
| Cost-sensitive hobby project | Fly.io (auto-stop + free tier) |
| Heroku migration | Render (most similar UX) |
Getting Started Checklist
Before deploying any Python API to production:
- All config comes from environment variables (no hardcoded values)
-
/healthendpoint returns 200 - Dockerfile runs as non-root user
- Database migrations are automated in the deploy process
-
uv.lockis 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