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¶
- Create a Railway project from your repo, and it detects
backend/Dockerfile. - Add a PostgreSQL service (New → Database → PostgreSQL); Railway provisions its connection string.
2. Deploy the backend (API)¶
- In the backend service → Variables, set the FastReact env (full list in Configuration):
FS_DB_URL: reference the Postgres service's connection stringFS_ENVIRONMENT=prod,FS_BASE_API_URL=https://api.yourdomain.com,FS_BASE_WEB_URL=https://app.yourdomain.comFS_JWT_SECRET_KEY,FS_CRON_SECRET, plus Stripe/email keys as needed
- Run migrations once against the prod database with
./sqitch.sh prod deploy(prod Sqitch target pointed atFS_DB_URL), from your local machine or CI. - 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 otherVITE_*) before the build. - Attach
app.yourdomain.comto the frontend andyourdomain.comto 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 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_URLmatch 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.