GenerateSaaS

Fly.io

Run the node deploy target's Docker image on Fly.io as a long-running Firecracker microVM.

Fly.io runs the node deploy target's standard Docker image as a long-running Firecracker microVM - there is no Fly-specific code in the boilerplate. It supports both fullstack and separate architectures, because the process stays alive between requests.

Build artifact

Fly builds the same image documented in Self-hosting with Docker - fly launch detects a Dockerfile and generates a fly.toml from it.

  • The owner image's CMD is pnpm start; the CLI prepends pnpm -F @repo/database migrate to that script, so the schema step runs on every machine start.
  • For separate, the owner image is apps/backend/Dockerfile, which EXPOSEs 3010 (API_PORT).
  • For fullstack, the owner image is the frontend app's Dockerfile, which EXPOSEs 3000 (PORT).

Set fly.toml's [http_service].internal_port to the image's exposed port: 3010 for separate, 3000 for fullstack. A mismatch makes Fly's proxy refuse all traffic.

Deploy

Provision database and cache. Either attach Fly Postgres (fly postgres create, then fly postgres attach, which injects DATABASE_URL) or use an external managed provider (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. It reads the owner Dockerfile, generates fly.toml, and creates the Fly app - decline the offer to deploy immediately so you can set secrets first.

Set secrets. Inject runtime env as Fly secrets:

fly secrets set \
  DATABASE_URL=… \
  REDIS_URL=… \
  BETTER_AUTH_SECRET=… \
  API_URL=… \
  INNGEST_APP_ID=… INNGEST_EVENT_KEY=… INNGEST_BASE_URL=…

See the full list in Environment variables.

Deploy. Run fly deploy. Fly builds the image, starts the microVM, and CMD pnpm start runs the schema migration before booting the long-running server. Confirm the deployed URL serves the app (and /api/* for fullstack).

Required runtime env

Set these as Fly secrets - the boot-time schema validators throw on a missing or invalid value.

SecretValidated byPurpose
DATABASE_URL@repo/databasePostgres connection; read by the CMD-time schema step and at runtime.
REDIS_URL@repo/runtimeCache + rate-limit store (or the Upstash pair if you chose managed cache).
BETTER_AUTH_SECRET@repo/runtimeAuth signing secret, min 32 chars (openssl rand -base64 32).
API_URL@repo/runtimeBackend origin the runtime calls; falls back to the *_PUBLIC_API_URL keys.
INNGEST_APP_ID@repo/runtimeBackground-jobs app identifier.
INNGEST_EVENT_KEY@repo/runtimeBackground-jobs event ingestion key.
INNGEST_BASE_URL@repo/runtimeBackground-jobs endpoint URL.

For separate, also set TRUSTED_ORIGINS on the backend - your production origin must be listed, since config.baseUrl is not auto-trusted. See Separate backend.

Fly is a deploy host, not a dev environment - never run pnpm infra against it. That compose stack (infra/docker-compose.yml) is local development only; in production you supply your own managed or self-hosted database and cache through secrets.

Next steps

On this page