---
title: "Build a Voice Agent on Railway: One-Click FastAPI Deploy (2026)"
description: "Ship a production voice agent in 5 minutes on Railway: FastAPI bridge, OpenAI Realtime, Postgres for sessions, and a one-click template. No Docker knowledge required."
canonical: https://callsphere.ai/blog/vw5h-build-voice-agent-railway-one-click-fastapi
category: "AI Voice Agents"
tags: ["Railway", "FastAPI", "OpenAI Realtime", "Twilio", "Tutorial"]
author: "CallSphere Team"
published: 2026-04-17T00:00:00.000Z
updated: 2026-05-07T16:30:08.584Z
---

# Build a Voice Agent on Railway: One-Click FastAPI Deploy (2026)

> Ship a production voice agent in 5 minutes on Railway: FastAPI bridge, OpenAI Realtime, Postgres for sessions, and a one-click template. No Docker knowledge required.

> **TL;DR** — Railway gives you Postgres, a Python service, environment variables, and a public HTTPS URL with two clicks. Drop in a FastAPI WebSocket bridge between Twilio and OpenAI Realtime, push to GitHub, and Railway redeploys on every commit. Total time from `git init` to live voice agent: under 5 minutes.

## What you'll build

A FastAPI service hosted on Railway that:

- Returns TwiML at `/incoming`
- Bridges `/media` WebSocket to OpenAI Realtime
- Logs every call to Railway-managed Postgres
- Auto-deploys on `git push`

## Prerequisites

1. Railway account (`railway login`).
2. GitHub repo.
3. `OPENAI_API_KEY`, Twilio number.
4. Python 3.11.

## Architecture

```mermaid
flowchart LR
  C[Caller] --> T[Twilio]
  T -->|HTTP TwiML| RW[Railway FastAPI]
  T -->|wss media| RW
  RW |wss| OAI[OpenAI Realtime]
  RW -->|asyncpg| PG[(Railway Postgres)]
  GH[GitHub repo] -->|push| RW
```

## Step 1 — FastAPI app

```python

# app.py

import os, json, base64, asyncio, asyncpg, websockets
from fastapi import FastAPI, WebSocket, Request
from fastapi.responses import Response

app = FastAPI()
pool: asyncpg.Pool

@app.on_event("startup")
async def startup():
    global pool
    pool = await asyncpg.create_pool(os.environ["DATABASE_URL"])
    async with pool.acquire() as c:
        await c.execute("""create table if not exists turns (
            id serial primary key, call_sid text, role text, text text, ts timestamptz default now())""")

@app.post("/incoming")
async def incoming(req: Request):
    host = req.headers["host"]
    return Response(content=f"""""",
                    media_type="text/xml")

@app.websocket("/media")
async def media(ws: WebSocket):
    await ws.accept()
    async with websockets.connect(
        "wss://api.openai.com/v1/realtime?model=gpt-realtime",
        additional_headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", "OpenAI-Beta": "realtime=v1"}
    ) as ai:
        await ai.send(json.dumps({
            "type": "session.update",
            "session": {
                "instructions": "You are a concise voice agent.",
                "voice": "marin",
                "input_audio_format": "g711_ulaw",
                "output_audio_format": "g711_ulaw",
                "turn_detection": {"type": "server_vad"}
            }
        }))
        sid = ""
        async def to_ai():
            async for raw in ws.iter_text():
                ev = json.loads(raw)
                nonlocal_sid = ev.get("streamSid")
                if ev.get("event") == "media":
                    await ai.send(json.dumps({"type": "input_audio_buffer.append", "audio": ev["media"]["payload"]}))
        async def to_caller():
            async for raw in ai:
                ev = json.loads(raw)
                if ev["type"] == "response.audio.delta":
                    await ws.send_text(json.dumps({"event": "media", "streamSid": sid, "media": {"payload": ev["delta"]}}))
                if ev["type"] == "response.done":
                    text = ev["response"]["output"][0]["content"][0]["transcript"]
                    async with pool.acquire() as c:
                        await c.execute("insert into turns(call_sid, role, text) values($1, 'assistant', $2)", sid, text)
        await asyncio.gather(to_ai(), to_caller())
```

## Step 2 — `requirements.txt` and `railway.toml`

```
fastapi==0.115.0
uvicorn[standard]==0.32.0
websockets==13.1
asyncpg==0.30.0
```

```toml

# railway.toml

[build]
builder = "NIXPACKS"
[deploy]
startCommand = "uvicorn app:app --host 0.0.0.0 --port $PORT"
restartPolicyType = "ON_FAILURE"
```

Railway's Nixpacks builder detects Python automatically; no Dockerfile needed.

## Step 3 — Provision Postgres + service

In Railway dashboard: New Project → Deploy from GitHub repo → pick the FastAPI repo. Add a Postgres plugin from the same project; Railway sets `DATABASE_URL` automatically.

## Step 4 — Set env vars

In the service settings, add `OPENAI_API_KEY`. Railway redeploys on save.

## Step 5 — Public URL + Twilio webhook

Railway generates `https://your-app.up.railway.app`. Plug into Twilio → number → Voice Webhook → `POST` `https://your-app.up.railway.app/incoming`.

## Step 6 — Add observability

Pin the Railway "OpenTelemetry" template, set `OTEL_EXPORTER_OTLP_ENDPOINT` to a Honeycomb/Tempo URL. Latency per turn shows up in spans automatically with the standard FastAPI OTel instrumentation.

## Step 7 — Scale

Bump replicas from 1 to N in the dashboard. Railway puts a load balancer in front; sticky sessions on `x-twilio-signature` keep call legs pinned.

## Pitfalls

- **Cold-start on free hobby plan** is ~5s — voice agents will drop. Use `Pro` ($5/mo + usage) or pin always-on.
- **Twilio retries** TwiML POSTs aggressively on slow boots; first request must respond <15s.
- **Postgres pool sizing**: Railway's Postgres has a connection cap; tune `asyncpg.create_pool(min_size=2, max_size=10)`.
- **Nixpacks Python version**: pin via `runtime.txt` or `PYTHON_VERSION=3.11` env var.
- **No persistent disk** on basic plans; logs are ephemeral. Pipe to Logtail/Axiom.

## How CallSphere does this in production

CallSphere doesn't run on Railway — we use bare k3s + Postgres on Hetzner for cost predictability at our scale (~$1k/mo infra for 6 verticals). For early-stage builders, Railway is the fastest way to ship a real voice agent with a real database. CallSphere's 37 agents, 90+ tools, 115+ DB tables, 6 verticals run on FastAPI :8084 with the same code patterns shown here. $149/$499/$1499, 14-day trial, 22% affiliate.

## FAQ

**Q: Railway vs Render vs Fly?**
Railway: easiest CLI + UI, Postgres bundled. Render: similar, slightly slower deploys. Fly: best for multi-region. Pick Railway for speed.

**Q: Can I use a one-click template?**
Yes — Railway's marketplace has `Deploy OpenAI Voice Assistant` and `Deploy Faster Whisper` templates that wire most of this for you.

**Q: Latency?**
Railway runs in `us-west` and `us-east`; voice-to-voice ~750ms vs Twilio + OpenAI on East Coast.

**Q: Cost at 100k call-min/month?**
Compute ~$30, Postgres ~$10, OpenAI Realtime ~$30k. Infra is rounding error — pick what's productive.

**Q: HIPAA?**
Railway doesn't sign BAAs as of May 2026. For HIPAA, run on AWS/GCP/Azure with their BAA.

## Sources

- [Deploy a FastAPI App — Railway Guides](https://docs.railway.com/guides/fastapi)
- [Deploy OpenAI Voice Assistant on Railway](https://railway.com/deploy/pCNq15)
- [Deploy Faster Whisper — Railway](https://railway.com/deploy/faster-whisper)
- [Deploy FastAPI WebSocket Chat — Railway](https://railway.com/deploy/fastapi-websocket-chat)
- [Deploying the example FastAPI app on Railway — Better Simple](https://www.better-simple.com/fastapi/2026/04/27/deploying-the-example-fastapi-app-on-railway/)

---

Source: https://callsphere.ai/blog/vw5h-build-voice-agent-railway-one-click-fastapi
