A production-ready, self-hosted Supabase stack built specifically for Railway. Fixes every common failure in existing templates.
What makes this different? — JWT keys are auto-generated on first boot (no manual steps) — All secrets share a single source via Railway reference variables (no mismatches) — Every Docker image is pinned to a known-good SHA (no surprise breakage) — Studio is locked to private networking only (no accidental public exposure)
Internet
│
▼
┌─────────────────┐ public Railway domain
│ Kong Gateway │ ← only service with a public URL
└────────┬────────┘
│ private network
┌────┴─────────────────────────────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌─────────┐
│ PostgREST│ │ GoTrue │ │ Realtime │ │ Storage │
│ (REST) │ │ (Auth) │ │ (Sockets) │ │ (S3) │
└────┬─────┘ └────┬─────┘ └─────┬─────┘ └────┬────┘
└──────────────┴──────────────┴─────────────┘
│
▼
┌─────────────────┐
│ Postgres 15 │ ← starts first
└─────────────────┘
┌─────────────────┐
│ Studio │ ← private network only
└─────────────────┘
Services: db · auth · rest · realtime · storage · studio · kong · init-jwt
Fork to your own GitHub account. Railway requires a public repo for templates.
Go to railway.com → New Project → Empty Project.
Go to Project Settings → Shared Variables and add these (Railway will
auto-generate secrets for variables that use ${{secret(N)}} syntax):
| Variable | Value |
|---|---|
JWT_SECRET |
${{secret(40)}} |
POSTGRES_PASSWORD |
${{secret(32)}} |
DASHBOARD_USERNAME |
supabase |
DASHBOARD_PASSWORD |
${{secret(20)}} |
ANON_KEY |
(leave blank — fill after Step 5) |
SERVICE_ROLE_KEY |
(leave blank — fill after Step 5) |
Add each folder as a separate Railway service pointing to your forked repo. Set the Root Directory for each service:
| Service name | Root Directory |
|---|---|
db |
/db |
auth |
/auth |
rest |
/rest |
realtime |
/realtime |
storage |
/storage |
studio |
/studio |
kong |
/kong |
init-jwt |
/init-jwt |
Paste the matching variables from service-variables.env.example into each
service's Variables tab.
Services will fail on first deploy — that's expected. Postgres isn't ready and ANON_KEY is empty. Continue to the next step.
Open the init-jwt service deploy logs. You'll see output like:
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5...
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5...
Copy both values. Go to Project Settings → Shared Variables and fill in
ANON_KEY and SERVICE_ROLE_KEY.
For each service below, go to its Variables tab and confirm these reference
variables are set. Railway autocompletes ${{ServiceName.RAILWAY_PRIVATE_DOMAIN}}:
DB_HOST = ${{db.RAILWAY_PRIVATE_DOMAIN}}
PGRST_PRIVATE_HOST = ${{rest.RAILWAY_PRIVATE_DOMAIN}}
GOTRUE_PRIVATE_HOST = ${{auth.RAILWAY_PRIVATE_DOMAIN}}
REALTIME_PRIVATE_HOST = ${{realtime.RAILWAY_PRIVATE_DOMAIN}}
STORAGE_PRIVATE_HOST = ${{storage.RAILWAY_PRIVATE_DOMAIN}}
Go to the kong service → Settings → Generate Domain.
Copy the domain (e.g. https://kong-production-xxxx.up.railway.app).
Update the Shared Variable:
SUPABASE_PUBLIC_URL = https://kong-production-xxxx.up.railway.app
SITE_URL = https://kong-production-xxxx.up.railway.app
Trigger a redeploy on all services. Deploy order that matters:
db— must be healthy firstauth,rest,realtime,storage— in any orderkong— last (needs all upstreams ready)studio— any time afterdb
# Test the REST API
curl https://your-kong-domain.up.railway.app/rest/v1/ \
-H "apikey: YOUR_ANON_KEY"
# Test Auth
curl https://your-kong-domain.up.railway.app/auth/v1/health
# Test Storage
curl https://your-kong-domain.up.railway.app/storage/v1/status \
-H "apikey: YOUR_ANON_KEY"All three should return 200. Studio is accessible via Railway's private network.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-kong-domain.up.railway.app', // SUPABASE_PUBLIC_URL
'YOUR_ANON_KEY' // shared.ANON_KEY
)That's it. Same API as Supabase Cloud.
GoTrue starts before Postgres is ready. Railway will auto-retry. Wait 60 seconds
and check again. If it keeps failing, check DB_HOST references the correct
private domain.
The most common cause: JWT_SECRET is different across services.
Fix: ensure every service references ${{shared.JWT_SECRET}}, not a
hardcoded value. Redeploy all services after fixing.
You are running supabase/studio:2025.10.09-sha-433e578 which has a known bug.
The Dockerfiles in this repo pin to 2025.10.01-sha-8460121 which is stable.
If you upgraded Studio manually, revert the image tag.
WebSocket connections require the API gateway to forward upgrade requests.
Ensure Kong is routing /realtime/v1/* to the realtime service.
Check Railway's WebSocket support docs if connections drop immediately.
If using local file storage, the storage service needs a Railway Volume mounted
at /var/lib/storage. Add a volume in the service settings.
For production, switch to S3 by setting STORAGE_BACKEND=s3 and the S3 vars.
When a new Supabase version releases:
- Test the new image tags in a new Railway environment (not production)
- Verify Studio SQL editor, Auth, and Storage all work
- Update the
FROMline in the affected Dockerfile - Merge and Railway will redeploy automatically
Never change a Dockerfile image tag directly in production without staging first.
Apache 2.0 — same as Supabase.