GenerateSaaS

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.

ArchitectureOwner imageExposed port
fullstackThe frontend app's Dockerfile3000 (PORT)
separateapps/backend/Dockerfile3010 (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/api

Fly 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.

SecretPurpose
DATABASE_URLPostgres connection, read by the CMD-time schema step and at runtime
REDIS_URLCache and rate-limit store, or the Upstash pair for managed cache
BETTER_AUTH_SECRETAuth signing secret, min 32 chars (openssl rand -base64 32)
API_URLBackend origin the runtime calls; falls back to the public API URL var
INNGEST_APP_ID, INNGEST_EVENT_KEY, INNGEST_BASE_URLBackground jobs
TRUSTED_PROXYNot 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.

On this page