Skip to content

Deploy to Railway

Railway is the simplest all-in-one path: it runs your FastAPI container, a managed PostgreSQL, and the static app + landing from one project, redeploying on every git push.

Outcome: API at api.yourdomain.com, app at app.yourdomain.com, landing at yourdomain.com.

1. Create the project + database

  1. Create a Railway project from your repo, and it detects backend/Dockerfile.
  2. Add a PostgreSQL service (New → Database → PostgreSQL); Railway provisions its connection string.

2. Deploy the backend (API)

  1. In the backend service → Variables, set the FastReact env (full list in Configuration):
    • FS_DB_URL: reference the Postgres service's connection string
    • FS_ENVIRONMENT=prod, FS_BASE_API_URL=https://api.yourdomain.com, FS_BASE_WEB_URL=https://app.yourdomain.com
    • FS_JWT_SECRET_KEY, FS_CRON_SECRET, plus Stripe/email keys as needed
  2. Run migrations once against the prod database with ./sqitch.sh prod deploy (prod Sqitch target pointed at FS_DB_URL), from your local machine or CI.
  3. Under Settings → Networking, add the custom domain api.yourdomain.com.

3. Deploy the app + landing (static)

frontend/ and landing/ are static React builds. Add a static service for each that runs npm install && npm run build and serves the build output:

  • Set VITE_API_BASE_URL=https://api.yourdomain.com (and any other VITE_*) before the build.
  • Attach app.yourdomain.com to the frontend and yourdomain.com to the landing.

Prefer Vercel for the static sites? The Vercel steps in Fly.io + Neon + Vercel apply to any backend host.

Serving the app from a sub-path

To serve the app at yourdomain.com/app instead of app.yourdomain.com: each Railway service has its own domain, so the service that owns yourdomain.com (the landing) forwards the prefix. In the landing's Caddyfile, add:

handle_path /app/* {
    reverse_proxy https://<frontend-service-domain>
}

handle_path strips the /app prefix, which is what the frontend service expects since it serves build/client at its own root. The frontend and backend settings that go with this are in Serving from a Sub-Path.

4. Wire it together

  • Point DNS for api, app, and the apex at the Railway domains.
  • Confirm FS_BASE_API_URL / FS_BASE_WEB_URL match the live URLs so CORS and cookies work (Configuration).
  • Add the Stripe webhook at https://api.yourdomain.com/webhooks/stripe (Billing & Subscriptions).
  • Review the Security checklist before launch.

Next steps

Security headers

Railway serves static sites with Caddy. Add a Caddyfile in frontend/ with a header block:

:{$PORT} {
    root * build/client
    try_files {path} /index.html
    file_server

    header {
        Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.yourdomain.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        Permissions-Policy "camera=(), microphone=(), geolocation=()"
    }
}

Replace https://api.yourdomain.com in connect-src with your real API URL, or the browser will block the app from calling it. See Security.