Module 7: Alternative Platforms (Render, Railway, Fly.io)
3. Railway: Deployment for AI Apps
Description
In this capsule you'll deploy your AI app on Railway. If Render is "modern Heroku," Railway is "Heroku reimagined for developers." Railway stands out for its exceptional developer experience: deploy from the CLI in one line, automatic previews per PR, integrated add-ons (PostgreSQL, Redis, MySQL, MongoDB) with one click, and a pricing model based on real usage (not a fixed plan). It's the platform many developers choose for prototypes that need to be online fast.
Context: You come from deploying on Render (capsule 02). The same app, the same goal — but a different platform, a different experience. The direct comparison gives you real data for your decision matrix. Railway does some things better than Render (CLI, add-ons, previews) and others worse (less predictable pricing, fewer fixed-plan options). By the end you'll have first-hand experience to evaluate.
Railway: Platform Overview
What Railway is
Railway is a cloud platform that deploys applications from Git or from the CLI. Its value proposition is speed and simplicity:
- Web Services: Apps with a server (any language/framework)
- Integrated add-ons: PostgreSQL, Redis, MySQL, MongoDB — one click to add
- Preview Environments: Every Pull Request generates a temporary deploy
- Powerful CLI:
railway upfrom your terminal deploys in seconds - Templates: Pre-configured apps (Next.js + Prisma, FastAPI + PostgreSQL, etc.)
Deployment model
Option A: From Git (automatic)
Your code (GitHub) → push to main → Railway detects → Build → Deploy → Public URL
Option B: From the CLI (manual)
Terminal → railway up → Railway packages and uploads → Build → Deploy → Public URL
Option C: From a template
Railway dashboard → Template → Click → Deploy → Public URL
Railway automatically detects your stack:
Dockerfile present → Docker build
requirements.txt → Python buildpack (Nixpacks)
package.json → Node buildpack (Nixpacks)
go.mod → Go buildpack
Cargo.toml → Rust buildpack
Pricing (data updated 2026)
Railway uses a usage-based pricing model, not fixed resource plans:
| Plan | Base price | Included credits | Usage |
|---|---|---|---|
| Trial | $0 | $5 of credit | 500 execution hours, no custom domains |
| Hobby | $5/month | $5 of credit (total $10) | Custom domains, deploy from Git |
| Pro | $20/month per seat | $10 of credit (total $30) | Teams, SLA, priority support |
Usage costs (after credits):
| Resource | Price |
|---|---|
| vCPU | $0.000463/min ($0.02778/hr) |
| RAM (GB) | $0.000231/min ($0.01388/hr) |
| Disk (GB) | $0.000308/min ($0.01852/hr) |
| Egress | $0.10/GB after 100 GB/month included |
Estimate for a typical AI app (1 vCPU, 1 GB RAM, 24/7):
- vCPU: $0.02778 × 730 hrs = ~$20/month
- RAM: $0.01388 × 730 hrs = ~$10/month
- Total infra: ~$30/month (Hobby: ~$25 after credits)
For AI workloads:
- Trial: Quick tests only. $5 of credit is consumed in ~1 week with an app running 24/7.
- Hobby: Viable for MVPs and personal projects. ~$20-30/month for a basic AI app.
- Pro: For teams. The same usage pricing, but with SLA and team features.
Limitations for AI
| Limitation | Impact on AI | Workaround |
|---|---|---|
| Trial: 500 hours | ~21 days of continuous execution | Upgrade to Hobby ($5/month) |
| Max 32 GB RAM (Pro) | Enough for most, not for XL models | Use external APIs for large models |
| Max 32 vCPU (Pro) | Enough for AI with external APIs | Don't run local inference of heavy models |
| Request timeout: 5 min | Better than Render (30s), but still limiting | Streaming for very long responses |
| No GPU | You can't run CUDA/local models | Inference APIs (OpenAI, Together AI) |
| Ephemeral disk by default | Data is lost on each deploy | Use persistent volumes (add-on) |
| Egress: 100 GB/month included | Enough for most AI apps | Monitor if you serve large files |
Deploy Step-by-Step: AI App on Railway
Step 1: Prepare and verify locally
cd deployment-cloud-guide/module-07/app
# Verify that the app works
docker build -t docusearch-ai .
docker run -p 8000:8000 -e OPENAI_API_KEY=sk-test docusearch-ai
curl http://localhost:8000/health
Step 2: Deploy from the CLI (the fastest way)
# Login (opens the browser for authentication)
railway login
# Create a new project
railway init
# ? Project name: docusearch-ai
# ✅ Project created: docusearch-ai
# Link to the current directory
railway link
# Configure environment variables
railway variables set OPENAI_API_KEY=sk-proj-xxx
railway variables set PLATFORM=railway
railway variables set LOG_LEVEL=info
# Deploy
railway up
# ✅ Uploading... done
# ✅ Building... done
# ✅ Deploying... done
# 🎉 https://docusearch-ai-production.up.railway.app
# Verify
curl https://docusearch-ai-production.up.railway.app/health
That's it. Three commands: railway init, railway variables set, railway up. Your app is online.
Step 3: Deploy from GitHub (automatic)
# In the Railway Dashboard:
# 1. New Project → Deploy from GitHub Repo
# 2. Select repository
# 3. Railway detects the Dockerfile automatically
# 4. Click "Deploy Now"
# Configure variables:
# Dashboard → your service → Variables → Raw Editor
# OPENAI_API_KEY=sk-proj-xxx
# PLATFORM=railway
From now on, every push to main generates an automatic deploy.
Step 4: Configure railway.toml
Railway allows declarative configuration with railway.toml:
# railway.toml
[build]
dockerfilePath = "Dockerfile"
[deploy]
startCommand = "uvicorn main:app --host 0.0.0.0 --port $PORT"
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3
git add railway.toml
git commit -m "Add Railway configuration"
git push origin main
# Railway detects the change and redeploys
Step 5: Generate a public domain
# From the CLI
railway domain
# ✅ Domain: docusearch-ai-production.up.railway.app
# Custom domain (requires Hobby+ plan)
# Dashboard → your service → Settings → Domains
# Add custom domain: api.your-domain.com
# Railway gives you a CNAME to configure in your DNS
Step 6: Verify the complete deployment
RAILWAY_URL="https://docusearch-ai-production.up.railway.app"
# Health check
curl $RAILWAY_URL/health
# Inference
curl -X POST $RAILWAY_URL/ask \
-H "Content-Type: application/json" \
-d '{"question": "What advantages does Railway have?", "max_tokens": 200}'
# Logs
railway logs
# 2026-03-08T15:30:00Z INFO Application startup complete
# 2026-03-08T15:30:05Z INFO POST /ask 200 1.2s
Railway: Add-ons and Services
PostgreSQL on Railway
# From the CLI
railway add
# ? Select a plugin: PostgreSQL
# ✅ PostgreSQL added to project
# Railway automatically injects:
# DATABASE_URL=postgresql://user:pass@host:port/dbname
# PGDATABASE, PGHOST, PGPASSWORD, PGPORT, PGUSER
From the dashboard:
Dashboard → your project → New → Database → PostgreSQL
Railway creates the instance and connects the variables automatically.
import os
DATABASE_URL = os.environ.get("DATABASE_URL")
# Railway injects this variable automatically
# You don't need to copy/paste the URL manually
Redis on Railway
railway add
# ? Select a plugin: Redis
# ✅ Redis added to project
# Railway injects:
# REDIS_URL=redis://default:pass@host:port
# REDISHOST, REDISPASSWORD, REDISPORT, REDISUSER
import os
import redis
REDIS_URL = os.environ.get("REDIS_URL")
cache = redis.from_url(REDIS_URL)
Multiple services in one project
Railway allows multiple services (monorepo):
Railway Project: docusearch-ai
├── Service: api (FastAPI)
│ └── Linked to: PostgreSQL, Redis
├── Service: worker (background processing)
│ └── Linked to: PostgreSQL, Redis
├── PostgreSQL instance
└── Redis instance
# Each service sees the variables of the linked services
# Internal communication uses Railway's private network
# You don't expose PostgreSQL to the internet — only the project's services access it
Railway: Preview Environments
One of Railway's most useful features for teams: each Pull Request generates a temporary deployment with its own URL and its own database instances.
main branch → Production deployment
https://docusearch-ai-production.up.railway.app
PR #42 → Preview deployment (automatic)
https://docusearch-ai-pr-42.up.railway.app
└── PostgreSQL temporary copy
└── Redis temporary copy
└── Inherited variables + overrides
Configure Preview Environments
Dashboard → your project → Settings → Environments
- Enable PR Deploys: ON
- Auto-deploy PR: ON
Each PR now generates an isolated deploy. When the PR is closed (merge or close), Railway destroys the preview automatically.
Per-environment variables
# Production variables
railway variables set OPENAI_API_KEY=sk-prod-xxx -e production
# Preview variables (staging)
railway variables set OPENAI_API_KEY=sk-staging-xxx -e staging
Railway CLI: Essential Commands
# Project
railway init # Create a new project
railway link # Link directory to an existing project
railway status # View project status
# Deploy
railway up # Deploy from the current directory
railway up --detach # Deploy without waiting
# Variables
railway variables # List all variables
railway variables set KEY=val # Add/update variable
railway variables delete KEY # Delete variable
# Services
railway add # Add an add-on (PostgreSQL, Redis, etc.)
# Logs and debug
railway logs # View logs in real time
railway logs --num 100 # Last 100 lines
# Domain
railway domain # Generate a public domain
# Environments
railway environment # List environments
railway environment production # Switch to production
# Connect to a database
railway connect postgres # Open psql connected to your PostgreSQL
railway connect redis # Open redis-cli connected to your Redis
# Run a remote command
railway run python manage.py migrate # Run in the service's context
Deployment Patterns on Railway for AI
Pattern 1: FastAPI + PostgreSQL + Redis
# Complete setup in 5 commands
railway init
railway add # → PostgreSQL
railway add # → Redis
railway variables set OPENAI_API_KEY=sk-xxx
railway up
# main.py — Railway injects DATABASE_URL and REDIS_URL automatically
import os
DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ["REDIS_URL"]
Pattern 2: Monorepo with API + Worker
# railway.toml for the API service
[build]
dockerfilePath = "Dockerfile.api"
[deploy]
startCommand = "uvicorn api.main:app --host 0.0.0.0 --port $PORT"
healthcheckPath = "/health"
# railway.toml for the worker (in another service of the same project)
[build]
dockerfilePath = "Dockerfile.worker"
[deploy]
startCommand = "python worker/main.py"
Pattern 3: Scheduled jobs for AI
Railway supports cron jobs for periodic tasks:
Dashboard → New Service → Cron Job
Schedule: 0 2 * * * (every day at 2 AM)
Command: python scripts/regenerate_embeddings.py
Troubleshooting
Problem 1: "railway up fails — it doesn't detect the project"
Solution: You need to link the directory to a Railway project first:
# If you haven't created a project
railway init
railway up
# If the project exists but isn't linked
railway link
# Select your project from the list
railway up
Problem 2: "Build fails — Nixpacks doesn't detect Python"
Solution: Railway uses Nixpacks by default. If you have a Dockerfile, make sure it's at the root or specified in railway.toml:
# railway.toml — force use of the Dockerfile
[build]
dockerfilePath = "Dockerfile"
If you don't want to use Docker, make sure requirements.txt is at the root:
ls
# main.py requirements.txt railway.toml
Problem 3: "App works but has no public URL"
Solution: Railway doesn't generate a domain automatically. You need to generate it:
railway domain
# ✅ https://your-app-production.up.railway.app
Or from the dashboard: Service → Settings → Networking → Generate Domain.
Problem 4: "Environment variables don't apply"
Solution: Verify that the variables are in the correct environment (production vs staging):
# View current variables
railway variables
# Verify active environment
railway environment
# Switch to production
railway environment production
railway variables
Problem 5: "The cost spiked — I don't understand the bill"
Solution: Railway charges by usage (CPU × time + RAM × time). An idle app with 1 vCPU and 1 GB RAM consumes ~$30/month. Options:
# View current usage
# Dashboard → your service → Metrics
# Reduce resources
# Dashboard → your service → Settings → Resource Limits
# Max vCPU: 0.5
# Max RAM: 512 MB
For apps that don't need to be 24/7, configure auto-sleep:
Dashboard → Service → Settings → Sleep after inactivity
Hands-On Exercises
Exercise 1: Deploy from the CLI on Railway
Deploy the same AI app you used on Render, but using the Railway CLI. Configure the variables and verify that it works.
See solution
cd deployment-cloud-guide/module-07/app
# Login
railway login
# Create project
railway init
# Name: docusearch-ai-railway
# Configure variables
railway variables set OPENAI_API_KEY=sk-proj-xxx
railway variables set PLATFORM=railway
# Deploy
railway up
# Generate URL
railway domain
# Verify
RAILWAY_URL=$(railway domain | grep "https")
curl $RAILWAY_URL/health
# {"status":"healthy","version":"1.0.0","platform":"railway"}
curl -X POST $RAILWAY_URL/ask \
-H "Content-Type: application/json" \
-d '{"question": "What advantages does Railway have over AWS?", "max_tokens": 200}'
# View logs
railway logs --num 20
Exercise 2: Add PostgreSQL and Redis with one command
Add PostgreSQL and Redis to your Railway project and verify that the variables are injected automatically.
See solution
# Add PostgreSQL
railway add
# Select: PostgreSQL
# Add Redis
railway add
# Select: Redis
# Verify that the variables were injected
railway variables
# DATABASE_URL=postgresql://...
# REDIS_URL=redis://...
# PGHOST=...
# PGPORT=...
# PGDATABASE=...
# PGUSER=...
# PGPASSWORD=...
# REDISHOST=...
# REDISPORT=...
# REDISUSER=...
# REDISPASSWORD=...
# Connect directly to the DB from your terminal
railway connect postgres
# psql (16.x)
# docusearch=> SELECT version();
# PostgreSQL 16.x ...
# Connect to Redis
railway connect redis
# 127.0.0.1:6379> PING
# PONG
# Verify from the app that the variables work
# Add a debug endpoint (for testing only):
@app.get("/debug/db")
async def debug_db():
import psycopg2
conn = psycopg2.connect(os.environ["DATABASE_URL"])
cur = conn.cursor()
cur.execute("SELECT version()")
version = cur.fetchone()[0]
conn.close()
return {"postgres_version": version}
# Redeploy with the new endpoint
railway up
# Verify
curl $RAILWAY_URL/debug/db
# {"postgres_version":"PostgreSQL 16.x on ..."}
Exercise 3: Compare deploy times — Render vs Railway
Write a script that measures the time from push until the app responds on both platforms. Document the difference.
See solution
# compare_deploy_times.py
import time
import subprocess
import requests
def measure_deploy_time(platform: str, url: str, push_command: str) -> dict:
"""Measures the time from push until the app responds."""
print(f"\n=== {platform} ===")
print(f"Running push...")
start = time.time()
subprocess.run(push_command, shell=True, capture_output=True)
push_time = time.time() - start
print(f"Push completed in {push_time:.1f}s")
print("Waiting for the app to respond...")
max_wait = 600 # 10 minutes max
poll_interval = 10
elapsed = 0
while elapsed < max_wait:
try:
resp = requests.get(f"{url}/health", timeout=5)
if resp.status_code == 200:
total_time = time.time() - start
print(f"App online after {total_time:.1f}s")
return {
"platform": platform,
"push_time_s": round(push_time, 1),
"total_deploy_time_s": round(total_time, 1),
"status": "success",
}
except requests.exceptions.RequestException:
pass
time.sleep(poll_interval)
elapsed += poll_interval
print(f" Waiting... ({elapsed}s)")
return {
"platform": platform,
"push_time_s": round(push_time, 1),
"total_deploy_time_s": max_wait,
"status": "timeout",
}
render_result = measure_deploy_time(
platform="Render",
url="https://docusearch-ai.onrender.com",
push_command="git push origin main",
)
railway_result = measure_deploy_time(
platform="Railway",
url="https://docusearch-ai-production.up.railway.app",
push_command="railway up",
)
print("\n=== Results ===")
print(f"Render: {render_result['total_deploy_time_s']}s")
print(f"Railway: {railway_result['total_deploy_time_s']}s")
diff = render_result["total_deploy_time_s"] - railway_result["total_deploy_time_s"]
faster = "Railway" if diff > 0 else "Render"
print(f"{faster} is {abs(diff):.0f}s faster")
## Typical results
| Metric | Render | Railway |
|---------|--------|---------|
| Push/upload | ~2s | ~5s (railway up packages locally) |
| Build | ~60-120s | ~30-90s |
| Deploy | ~30-60s | ~10-30s |
| **Total** | **~120-180s** | **~60-120s** |
Railway tends to be faster because Nixpacks has aggressive caching
and the deploy pipeline is shorter. Render prioritizes stability over
speed in the pipeline.
Exercise 4: Configure a Preview Environment for your app
Configure Preview Environments on Railway so that each PR generates an isolated deploy with its own database.
See solution
# 1. In the Railway Dashboard:
# Project → Settings → Environments
# - Enable PR Deploys: ON
# 2. Create a feature branch
git checkout -b feature/add-streaming
# Add a streaming endpoint to main.py
from fastapi.responses import StreamingResponse
import json
import asyncio
@app.post("/ask/stream")
async def ask_stream(query: Query):
async def generate():
api_key = os.environ.get("OPENAI_API_KEY")
import openai
client = openai.OpenAI(api_key=api_key)
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a documentation assistant."},
{"role": "user", "content": query.question},
],
max_tokens=query.max_tokens,
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
yield f"data: {json.dumps({'token': token})}\n\n"
await asyncio.sleep(0)
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
# 3. Push the branch
git add -A
git commit -m "Add streaming endpoint"
git push origin feature/add-streaming
# 4. Create a PR on GitHub
gh pr create --title "Add streaming endpoint" --body "Adds /ask/stream with SSE"
# 5. Railway detects the PR and creates a preview automatically
# The dashboard shows:
# Production: https://docusearch-ai-production.up.railway.app
# PR #1: https://docusearch-ai-pr-1.up.railway.app
# 6. Verify the preview
curl https://docusearch-ai-pr-1.up.railway.app/health
# 7. The preview has its own PostgreSQL and Redis instance
# Production data is NOT affected
# 8. On merging the PR, Railway destroys the preview automatically
Summary
- Railway prioritizes developer experience:
railway updeploys in seconds from your terminal. - Usage-based pricing is flexible but less predictable than Render — a 24/7 app with 1 vCPU + 1 GB RAM costs ~$25-30/month.
- Integrated add-ons (PostgreSQL, Redis) are added with one command and the variables are injected automatically.
- Preview Environments are the flagship feature for teams: each PR generates an isolated deploy with its own DB.
- The Railway CLI is more powerful than Render's:
railway connect postgresopens psql directly to your database. - The 5-minute request timeout is better than Render (30s) for AI workloads with long responses.
- Trial ($5 credit) is consumed fast. Hobby ($5/month + usage) is the minimum viable option.
- The fastest pattern:
railway init+railway add(PostgreSQL) +railway variables set+railway up.
Additional Resources
- Railway Documentation — Complete official documentation
- Railway CLI Reference — Complete CLI command reference
- Railway Templates — Pre-configured templates for fast deploy
- Railway Nixpacks — The build system Railway uses by default
- Railway Pricing Calculator — Usage-based pricing calculator
- Railway Changelog — Platform news and updates