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 up from 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:

PlanBase priceIncluded creditsUsage
Trial$0$5 of credit500 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):

ResourcePrice
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

LimitationImpact on AIWorkaround
Trial: 500 hours~21 days of continuous executionUpgrade to Hobby ($5/month)
Max 32 GB RAM (Pro)Enough for most, not for XL modelsUse external APIs for large models
Max 32 vCPU (Pro)Enough for AI with external APIsDon't run local inference of heavy models
Request timeout: 5 minBetter than Render (30s), but still limitingStreaming for very long responses
No GPUYou can't run CUDA/local modelsInference APIs (OpenAI, Together AI)
Ephemeral disk by defaultData is lost on each deployUse persistent volumes (add-on)
Egress: 100 GB/month includedEnough for most AI appsMonitor 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 up deploys 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 postgres opens 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

  1. Railway Documentation — Complete official documentation
  2. Railway CLI Reference — Complete CLI command reference
  3. Railway Templates — Pre-configured templates for fast deploy
  4. Railway Nixpacks — The build system Railway uses by default
  5. Railway Pricing Calculator — Usage-based pricing calculator
  6. Railway Changelog — Platform news and updates