Fly.io
Run the node deploy target's Docker image on Fly.io as a long-running Firecracker microVM, with secrets, an internal port, and migrate-on-boot.
Fly.io runs the node target's standard Docker image as a long-running Firecracker microVM. There is no Fly-specific code in the boilerplate, and both fullstack and separate work because the process stays alive between requests.
Build artifact
fly launch detects the owner app's Dockerfile and generates a fly.toml from it - the same image documented in Self-hosting with Docker.
| Architecture | Owner image | Exposed port |
|---|---|---|
fullstack | The frontend app's Dockerfile | 3000 (PORT) |
separate | apps/backend/Dockerfile | 3010 (API_PORT) |
The image's CMD is pnpm start, with pnpm -F @repo/database run deploy prepended by the CLI, so the schema step runs on every machine start.
Set fly.toml's [http_service].internal_port to the image's exposed port - 3000 for fullstack, 3010 for separate. A mismatch makes Fly's proxy refuse all traffic.
Deploy
Provision database and cache. Attach Fly Postgres (fly postgres create, then fly postgres attach, which injects DATABASE_URL) or use Neon/Supabase. For cache, run a Redis machine or use Upstash. See Database and Caching.
Launch the app. Run fly launch in the repo root and decline the offer to deploy immediately, so you can set secrets first.
Set secrets.
fly secrets set \
DATABASE_URL=… \
REDIS_URL=… \
BETTER_AUTH_SECRET=… \
API_URL=… \
INNGEST_APP_ID=… INNGEST_EVENT_KEY=… INNGEST_BASE_URL=…Deploy. For a frontend image, pass the public API URL as a build arg - Next.js bakes NEXT_PUBLIC_* into the browser bundle at build time, so a runtime secret cannot fix it later (see Self-hosting with Docker).
fly deploy --build-arg NEXT_PUBLIC_API_URL=https://yourdomain.com/apiFly builds the image and starts the microVM; CMD pnpm start migrates before booting the server.
Required secrets
Boot-time validators throw on a missing or invalid value.
| Secret | Purpose |
|---|---|
DATABASE_URL | Postgres connection, read by the CMD-time schema step and at runtime |
REDIS_URL | Cache and rate-limit store, or the Upstash pair for managed cache |
BETTER_AUTH_SECRET | Auth signing secret, min 32 chars (openssl rand -base64 32) |
API_URL | Backend origin the runtime calls; falls back to the public API URL var |
INNGEST_APP_ID, INNGEST_EVENT_KEY, INNGEST_BASE_URL | Background jobs |
TRUSTED_PROXY | Not needed - the backend recognises Fly from its own FLY_APP_NAME variable. See Client IP |
On separate, also set TRUSTED_ORIGINS on the backend to your production origin - config.baseUrl is not auto-trusted. See Separate backend and Environment variables.
Fly is a deploy host, not a dev environment - never run pnpm infra against it. That compose stack is local development only; in production you supply your own database and cache through secrets.
Railway
Deploy the node target to Railway straight from your Git repo, with managed Postgres and Redis services and the schema migrating on every boot.
Render
Run the node deploy target on Render as a managed Web Service built from the owner app's Dockerfile, with attached Postgres and a Key Value cache.