Production deployment
Last Updated: 2026-06-11
This page describes how to run Substratum’s gateway and file-explorer UI in production. It mirrors the local Compose edge pattern (one public origin for UI, API, and OAuth metadata), but with HTTPS, static assets, and managed services instead of Vite dev server.
Design goal: one browser origin
The browser, OAuth redirect URI, session cookie, and discoverable client metadata must all use the same public origin — the value of PUBLIC_BASE_URL (no trailing slash).
Request routing
Match docker/nginx-edge.conf in production:
| Path | Handler | Notes |
|---|---|---|
/ | Static SPA (index.html, JS assets) | Build with nx run file-explorer:build; configure SPA fallback for client routes |
/api/* | Gateway | Include X-Forwarded-Proto, Host, client IP headers |
/.well-known/* | Gateway | Required for OAuth client_id document |
Release artifacts
| Component | Artifact | Notes |
|---|---|---|
| Gateway | Dockerfile.gateway runtime stage → substratum-gateway binary | Runs as nobody; bind HOST/PORT (default 0.0.0.0:18080 internally) |
| File explorer | dist/apps/file-explorer/ from Nx production build | Served by CDN/nginx, not embedded in gateway today |
| Database | Managed PostgreSQL | Provision infra/data; bootstrap via data deployment; app pool uses substratum_gateway (RLS) |
| Blocks | S3-compatible storage or FlatFS | Set S3_* in production; FlatFS is for small/self-host only |
Required configuration
Set via environment or substratum-gateway.json (env wins on conflict):
| Variable | Production notes |
|---|---|
PUBLIC_BASE_URL | https://app.example.com — must match TLS cert and the URL users open |
DATABASE_URL | App role URL from infra/data stack output databaseUri — gateway, ops-api, pds-authz-proxy runtime |
DATABASE_ADMIN_URL | **doadmin admin URI from infra/data stack output databaseAdminUri — migrations/bootstrap only, not gateway runtime |
JWT_SECRET | Strong random secret; no debug fallback in production |
SWARM_MASTER_SECRET | Required in production |
LIBP2P_IDENTITY_KEY | Stable base64 libp2p keypair |
CORS_ALLOWED_ORIGINS | Same origin as SPA if UI and API share PUBLIC_BASE_URL; add extra origins only for separate admin UIs |
HOST / PORT | Gateway listen address behind the proxy |
Optional PDS-related vars apply when you operate a Substratum-hosted PDS or signup proxy — see OAuth and PDS origins. For sign-in against public networks (Bluesky handles), users’ home PDSes are resolved at OAuth time; you do not run a local PDS container.
Pre-deploy checklist
- DNS + TLS — Certificate covers the exact host in
PUBLIC_BASE_URL(pick apex orwww, redirect the other). - Build UI —
pnpm installthennx run file-explorer:build; uploaddist/apps/file-explorerto static hosting or mount in nginx. - Build gateway —
docker build -f Dockerfile.gateway --target runtime -t substratum-gateway . - Migrations —
sea-orm-cli migrate upwithDATABASE_ADMIN_URL(one-off deploy step, not on gateway startup). - Smoke tests
GET https://app.example.com/.well-known/oauth-client-metadata.jsonreturns JSON;client_idmatches that URL.GET https://app.example.com/api/v1/health(or drives with 401) reaches gateway through proxy.- Complete OAuth login; confirm
substratum_sessioncookie onPUBLIC_BASE_URLorigin withHttpOnlyandSecure(whenPUBLIC_BASE_URLishttps://).
- Secrets — No
.envin the image; inject via platform secret store (ADR 13).
Differences from local Compose
| Local dev | Production |
|---|---|
Vite on :14200 (Compose-internal) | Static dist/ |
nginx edge on http://127.0.0.1:8080 | HTTPS reverse proxy / CDN |
| Loopback OAuth client metadata | Discoverable HTTPS metadata |
Optional local PDS on :3000 | Users’ federated PDSes |
| Devcontainer forwards 8080 only | Public URL is the only entry point |
See also
- Data (managed Postgres) —
infra/data, bootstrap,DATABASE_URL - App edge deployment — Pulumi
infra/app, rsync, smoke tests - OAuth and PDS origins
- Getting started — local edge
apps/gateway/AGENTS.md