All writing
fastapi deployment railway docker python

Mastering FastAPI Deployment on Railway – From Docker to CI/CD

7 min read
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

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"]

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}"]

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:

NameValue (example)
DATABASE_URLpostgresql://user:pass@host/db
SECRET_KEYsuper‑secret‑key
LOG_LEVELinfo
UVICORN_WORKERS4

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,
)

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

Alembic migrations

Initialize Alembic once locally:

Terminal window
alembic init alembic

Edit alembic.ini to use the same DATABASE_URL:

sqlalchemy.url = env:DATABASE_URL

Create a migration:

Terminal window
alembic revision --autogenerate -m "Create users table"
alembic upgrade head

Running migrations on Railway

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

Terminal window
alembic upgrade head

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

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.

SymptomLikely CauseFix
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 PostgreSQLDATABASE_URL not injected or wrong format.Check Railway Variables tab; ensure the URL matches postgresql://user:pass@host:port/db.
High memory usage, workers killedToo many Gunicorn workers for the plan.Reduce UVICORN_WORKERS to 2 or 3, or upgrade the Railway plan.
Migrations not appliedDeploy hook missing or failing silently.Add set -e at the start of the hook script, or view logs under Deploy → Logs.
AsyncSession leaksSessions 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!

Working on something similar?

If you're building backend or AI systems and want a second set of senior eyes, let's talk.

Keep reading

Related articles