DEV Community

Cover image for Mastering FastAPI Deployment on Railway – From Docker to CI/CD
Ayush Kumar
Ayush Kumar

Posted on • Originally published at logiclooptech.dev

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

  1. Sign up / log in to railway.app. The free tier gives you 500 GB‑hours of compute and a PostgreSQL instance.
  2. Click New Project → Deploy from GitHub.
  3. 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.yml
Enter fullscreen mode Exit fullscreen mode

2. 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 wheels
RUN 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 pip
COPY 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 builder
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# Copy application code
COPY app ./app
COPY alembic ./alembic
COPY alembic.ini .

# Set environment variables for production
ENV 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 workers
CMD ["gunicorn", "app.main:app", \
     "--workers", "4", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000", \
     "--log-level", "info"]
Enter fullscreen mode Exit fullscreen mode

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 gcc and libpq-dev only in the builder stage ensures the runtime image stays minimal.
  • The CMD uses 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}"]
Enter fullscreen mode Exit fullscreen mode

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

# app/database.py
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,
)
Enter fullscreen mode Exit fullscreen mode

If you run into session‑related errors, see our guide on Fix AsyncSession Errors in FastAPI.

Alembic migrations

Initialize Alembic once locally:

alembic init alembic
Enter fullscreen mode Exit fullscreen mode

Edit alembic.ini to use the same DATABASE_URL:

sqlalchemy.url = env:DATABASE_URL
Enter fullscreen mode Exit fullscreen mode

Create a migration:

alembic revision --autogenerate -m "Create users table"
alembic upgrade head
Enter fullscreen mode Exit fullscreen mode

Running migrations on Railway

Add a Railway Deploy Hook (Settings → Deploy → Deploy Hook) with the command:

alembic upgrade head
Enter fullscreen mode Exit fullscreen mode

Railway 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 up
Enter fullscreen mode Exit fullscreen mode

How 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). The railway up command 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 Dockerfile and a DATABASE_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 $PORT variable 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!

Top comments (0)