Mastering FastAPI Deployment on Railway – From Docker to CI/CD
Introduction
If you’ve been searching for a frictionless way to get a fastapi deployment on railway, you’re in the right place. Railway provides a managed platform that spins up environments in seconds, gives you a free PostgreSQL instance, and integrates tightly with GitHub. In this post we’ll walk through the entire lifecycle: create a Railway project, Dockerize your FastAPI app, configure workers and environment variables, hook up a Railway‑hosted PostgreSQL database, and automate everything with a GitHub Actions CI/CD pipeline. By the end you’ll have a production‑ready FastAPI service that can be updated with a single git push.
TL;DR – The focus keyword “fastapi deployment on railway” appears naturally throughout the guide, ensuring both SEO relevance and a clear learning path.
1. Creating a Railway Project and Linking a GitHub Repo
- Sign up / log in to railway.app. The free tier gives you 500 GB‑hours of compute and a PostgreSQL instance.
- Click New Project → Deploy from GitHub.
- Authorize Railway to access your repositories, then select the repo that will host the FastAPI code (or create a new one).
Railway will automatically detect a Dockerfile or a requirements.txt. Since we’ll be using Docker, make sure the repo contains a Dockerfile at the root. After the first push, Railway builds the image, runs it, and provides a public URL like https://fastapi-demo.up.railway.app.
Quick repo skeleton
fastapi-railway/├─ app/│ ├─ main.py│ └─ models.py├─ alembic/│ └─ ... (migration files)├─ requirements.txt├─ Dockerfile└─ .github/ └─ workflows/ └─ ci.yml2. Dockerizing a FastAPI App for Railway
Railway’s build environment expects a multi‑stage Dockerfile that produces a lean final image. Below is a production‑ready Dockerfile that installs dependencies, compiles any binary wheels, and runs the app with Uvicorn behind Gunicorn.
# ---- Build stage ---------------------------------------------------------FROM python:3.12-slim AS builder
# Install build‑essential packages required for some wheelsRUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev && \ rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install poetry (optional) or just pipCOPY requirements.txt .RUN pip install --upgrade pip && \ pip install --no-cache-dir -r requirements.txt
# ---- Runtime stage -------------------------------------------------------FROM python:3.12-slim
WORKDIR /app
# Copy only the compiled dependencies from builderCOPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packagesCOPY --from=builder /usr/local/bin /usr/local/bin
# Copy application codeCOPY app ./appCOPY alembic ./alembicCOPY alembic.ini .
# Set environment variables for productionENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ UVICORN_WORKERS=4 \ LOG_LEVEL=info
# Expose the port Uvicorn will listen on (Railway uses $PORT)EXPOSE 8000
# Entrypoint runs Gunicorn with Uvicorn workersCMD ["gunicorn", "app.main:app", \ "--workers", "4", \ "--worker-class", "uvicorn.workers.UvicornWorker", \ "--bind", "0.0.0.0:8000", \ "--log-level", "info"]Why this Dockerfile works well on Railway
- Multi‑stage builds keep the final image under 100 MB, which speeds up Railway’s build cache.
- Installing
gccandlibpq-devonly in the builder stage ensures the runtime image stays minimal. - The
CMDuses Gunicorn to manage multiple Uvicorn workers, a pattern discussed in depth in our FastAPI in Production guide.
3. Configuring Uvicorn/Gunicorn Workers and Environment Variables
Railway injects a PORT environment variable at runtime. To make the container flexible, we read it inside the Dockerfile command:
CMD ["gunicorn", "app.main:app", \ "--workers", "${UVICORN_WORKERS:-2}", \ "--worker-class", "uvicorn.workers.UvicornWorker", \ "--bind", "0.0.0.0:${PORT:-8000}", \ "--log-level", "${LOG_LEVEL:-info}"]Tuning workers
A rule of thumb: workers = (2 × CPU cores) + 1. Railway’s free tier gives you a single vCPU, so 4 workers is a safe default. If you upgrade to a larger plan, bump the UVICORN_WORKERS variable accordingly.
Managing secrets
Railway’s UI lets you add environment variables under Settings → Variables. Add:
| Name | Value (example) |
|---|---|
DATABASE_URL | postgresql://user:pass@host/db |
SECRET_KEY | super‑secret‑key |
LOG_LEVEL | info |
UVICORN_WORKERS | 4 |
These variables are automatically injected into the container at start‑up, so you never need to hard‑code credentials.
4. Connecting to Railway‑Hosted PostgreSQL and Managing Migrations
Railway creates a PostgreSQL plugin that you can attach to your project. Once attached, the DATABASE_URL variable appears in the environment.
Using SQLAlchemy 2.0 with async sessions
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine, async_sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL")engine: AsyncEngine = create_async_engine( DATABASE_URL, echo=False, future=True,)
AsyncSessionLocal = async_sessionmaker( bind=engine, expire_on_commit=False, class_=AsyncSession,)If you run into session‑related errors, see our guide on Fix AsyncSession Errors in FastAPI.
Alembic migrations
Initialize Alembic once locally:
alembic init alembicEdit alembic.ini to use the same DATABASE_URL:
sqlalchemy.url = env:DATABASE_URLCreate a migration:
alembic revision --autogenerate -m "Create users table"alembic upgrade headRunning migrations on Railway
Add a Railway Deploy Hook (Settings → Deploy → Deploy Hook) with the command:
alembic upgrade headRailway runs this hook after each successful build, guaranteeing the DB schema stays in sync.
5. CI/CD Pipeline with GitHub Actions for Automatic Deployments
Railway already builds on each push, but a CI pipeline lets you run tests, linting, and static analysis before the image reaches production.
Create .github/workflows/ci.yml:
name: CI / Deploy to Railway
on: push: branches: [ main ] pull_request: branches: [ main ]
jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: test ports: [5432:5432] options: >- --health-cmd "pg_isready -U test" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest - name: Run tests env: DATABASE_URL: postgresql://test:test@localhost:5432/test run: pytest -vv
deploy: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to Railway uses: railwayapp/cli-action@v1 with: railway_token: ${{ secrets.RAILWAY_TOKEN }} command: railway upHow it works
- test – Spins up a PostgreSQL container, runs your unit tests, and only proceeds if they pass.
- deploy – Uses the official Railway CLI Action. Store your Railway API token as a GitHub secret (
RAILWAY_TOKEN). Therailway upcommand pushes the latest commit, triggers the Docker build, runs migrations, and swaps the live instance.
With this pipeline, any broken commit is caught early, and the “fastapi deployment on railway” process becomes fully automated.
6. Troubleshooting Common Railway Deployment Issues
Even with a solid setup, you may hit snags. Below are the most frequent problems and how to resolve them.
| Symptom | Likely Cause | Fix |
|---|---|---|
Error: No module named 'uvicorn' | requirements.txt missing uvicorn or Docker build cache stale. | Verify uvicorn is listed, then trigger a fresh Railway build (railway reset && railway up). |
Connection refused to PostgreSQL | DATABASE_URL not injected or wrong format. | Check Railway Variables tab; ensure the URL matches postgresql://user:pass@host:port/db. |
| High memory usage, workers killed | Too many Gunicorn workers for the plan. | Reduce UVICORN_WORKERS to 2 or 3, or upgrade the Railway plan. |
| Migrations not applied | Deploy hook missing or failing silently. | Add set -e at the start of the hook script, or view logs under Deploy → Logs. |
AsyncSession leaks | Sessions not closed after request. | Use a dependency that yields a session and ensures await session.close(). See our article on FastAPI Session Leak Detection. |
When Railway’s UI shows a red “Failed” badge, click View Logs. The logs include the Docker build output and the runtime stdout/stderr, making it easy to pinpoint missing env vars or syntax errors.
Key Takeaways
- Railway provides a zero‑config environment; you just need a proper
Dockerfileand aDATABASE_URL. - Docker multi‑stage builds keep images small and fast to rebuild, which is crucial for the free tier’s build limits.
- Run Gunicorn with Uvicorn workers and expose the
$PORTvariable to let Railway route traffic correctly. - Manage database schema with Alembic and automate migrations via a Railway Deploy Hook.
- A lightweight GitHub Actions pipeline adds testing and guarantees that only passing code reaches production.
- Common pitfalls—missing dependencies, wrong env vars, worker over‑provisioning—are quickly solvable by checking Railway’s logs and adjusting the Dockerfile or environment settings.
With these steps, you now have a reliable fastapi deployment on railway that scales from a hobby project to a production service without leaving the comfort of your favorite editor. Happy coding!
Working on something similar?
If you're building backend or AI systems and want a second set of senior eyes, let's talk.