Deploying FastAPI on Railway

How I containerized a FastAPI optimization API, deployed it from GitHub to Railway, exposed /solve, and connected a separate frontend to the public endpoint.

Written in February 2026. The monthly cost is a snapshot from then.

This deployment serves an optimization backend, the scheduler from my repair-shop scheduling work, from the public POST /solve endpoint. The API code lives in app/, the repository includes a Dockerfile, and Railway builds that container after each GitHub deployment. A separate frontend sends the model input to the public API URL and renders the solution.

I had already deployed FastAPI on ECS and on Lambda (joblab-agent-api). For this small, continuously available API, Railway reduced the amount of infrastructure I needed to operate while keeping the container boundary explicit.

From local process to public container

Local FastAPI app, git push to GitHub, Railway builds the Dockerfile, public Railway domain; the frontend calls that domain

The deployment path is short, but every boundary is testable:

  1. Start app.main:app locally with uvicorn app.main:app --reload.
  2. Exercise GET /, GET /docs, and POST /solve.
  3. Push the repository to GitHub.
  4. Let Railway detect the root Dockerfile and build the image.
  5. Generate a public domain and repeat the endpoint tests against it.

The OpenAPI page at /docs is especially useful here because it verifies request validation and the deployed route before the frontend is connected.

Watch the video walkthrough on YouTube

Runtime and cost are deployment settings

Railway can stop an idle service when serverless mode is enabled. That reduces compute usage for a demonstration API, but the first request after sleep can take longer. An always-active service removes that cold-start tradeoff and consumes resources continuously.

My small instance was around $1 per month at the time of measurement. That number is not part of the architecture and is not a forecast. Request volume, memory, CPU time, egress, plan rules, and current Railway pricing determine the real bill.

What I monitor after deployment

A successful build is only the first check. I use Railway’s service view to inspect application logs, CPU, memory, restarts, and resource estimates. I then call /docs and /solve from outside Railway so the test covers the public domain, TLS termination, container process, FastAPI validation, and solver response.

Connect the frontend through configuration

The frontend should not contain a hard-coded production hostname. It reads the solver endpoint from an environment variable:

NEXT_PUBLIC_API_URL=https://your-railway-domain/solve

For local development, the same client can target:

http://127.0.0.1:8000/solve

This keeps frontend builds environment-specific without changing application code. If you adapt the project, update the request schema on both sides and configure FastAPI CORS for the exact frontend origins you deploy.

What this project demonstrates

The useful part of this example is the complete connection between a model, an HTTP contract, a reproducible container, a public runtime, and a separately deployed frontend. Railway removes several infrastructure steps, but the deployment still depends on correct process binding, environment configuration, CORS, endpoint validation, logs, and cost monitoring.

How to repeat this

The backend is BigPictureFastAPI and the client is BigPictureFrontend.

git clone https://github.com/oploy-eu/BigPictureFastAPI
cd BigPictureFastAPI
pip install -r requirements.txt
uvicorn app.main:app --reload
  1. Open http://127.0.0.1:8000/docs and send one request to POST /solve from there.
  2. Push the repository to your own GitHub account and create a Railway service from it. Railway finds the root Dockerfile.
  3. Generate a public domain and repeat step 1 against it.
  4. Set NEXT_PUBLIC_API_URL in the frontend to that domain plus /solve, and add the frontend origin to the FastAPI CORS settings.

The live optimization demo shows the finished result. For the database side of the platform, see PostgreSQL on Railway.

What went wrong

I wrote above that the frontend should not contain a hard-coded hostname. My own frontend breaks that rule. In src/lib/usePostSolve.ts, the client falls back to the old Railway URL when NEXT_PUBLIC_API_URL is not set, and that Railway service no longer exists (it returns 404). The live demo still works because its build sets the variable and now calls a self-hosted endpoint. If you fork the frontend, remove the fallback so a missing variable fails loudly.

← All notes