Next.js hosting on Fly.io: a full-stack React framework on global infrastructure

Fly.io is a developer-centric, global public cloud with everything you need to deploy your Next.js app at scale.

Launch Now

Ready, Set, Go!

Speedrun Your Next.js App Onto Fly.io

Deploy a Next.js app to Fly.io in a few minutes with Fly.io's command line tool, flyctl.

Get Started
> Install flyctl on GNU/Linux
curl -L https://fly.io/install.sh | sh
 
> Run from Next.js project root
fly launch
 
> Scale CPU, memory, instances & regions
fly scale

Not on GNU/Linux? Install flyctl for your platform.

Everything on the Edge – Including Data

Launch your Next.js app – any and everything it depends on – in the regions closest to your users. Fly.io partners offering globally-distributed databases, message queues, and S3-compatible object storage. Distance is no issue when your whole app can be everywhere.

Private Networks, WireGuard

Fly Apps get their own private network by default, making it easier than ever to connect your services on other platforms securely. Additionally, using WireGuard you can easily connect existing services on other networks to your apps on Fly.io.

NEW!

Managed Postgres for Next.js Projects

Focus on features and leave database management to Fly.io. Our Managed Postgres service, gives you:

  • Automatic backups and recovery
  • High availability with automatic failover
  • Performance monitoring and metrics
  • Resource scaling (CPU, RAM, storage)
  • 24/7 support and incident response
  • Automatic encryption of data at rest and in transit
Learn More

Only Pay for What You Use

Get the benefits of a full server without the overhead. With fast-booting VMs that automatically start and stop when they need to, you don't have to worry about paying for resources not in use.

  • Bare Metal – Not a Big Cloud Abstraction

    No longer do you have to choose between great developer experience and reasonable pricing. Fly.io is its own cloud with physical servers in data centers; scale your apps without losing sleep over accidentally going viral.

  • Generated Dockerfiles for SSG & SSR

    Whether you're serving static pages or rendering content server-side, we make it easy to generate Dockerfiles for optimized images so you can focus on building amazing apps.

  • Globally Distributed Object Storage, No CDN Required

    Tigris Data is an S3-compatible object storage solution that stores assets globally by default. Setup a bucket in seconds with a single command. Great developer experience. Zero egress costs.

  • Managed Data Services for Every Occasion

    Keep your data closest to your users. Supabase for Postgres. LiteFS for SQLite. Upstash for Redis®. We partner with managed data services on top of our infrastructure for extremely low-latency.

Deploy a Next.js app to Fly.io: the actual flow

The flow is three commands: install flyctl, run fly launch, then fly deploy for every change after that.

Install the CLI:

curl -L https://fly.io/install.sh | sh

flyctl is the only tool you need locally. Authenticate with fly auth login once.

Kick off your first deploy from your Next.js project root:

cd path/to/your/nextjs/app
fly launch

fly launch detects Next.js by reading package.json, picks a region close to you, generates a Dockerfile and a fly.toml in your repo, and offers to provision a Postgres database and Redis. If you've set output: 'standalone' in next.config.js (which we recommend; see the next section), the generated Dockerfile uses that layout for a smaller image.

For every change after the first deploy, run fly deploy. That rebuilds the image, pushes it to our registry, and rolls a new version onto your Machines with no downtime.

The generated fly.toml has an http_service block that maps your container's PORT to Fly.io's edge proxy. The generated Dockerfile installs your node_modules, runs npm run build (or yarn / pnpm equivalent), and starts the Next.js server on that PORT.

Use standalone output for smaller images

Standalone output is a Next.js build mode that produces a self-contained .next/standalone directory with only the runtime files your app needs. The result is a smaller Docker image, faster cold starts, and no devDependencies in the deployed Machine.

Enable it in next.config.js:

module.exports = {
  output: 'standalone',
}

If you set this before running fly launch, the generated Dockerfile uses the standalone layout (copies .next/standalone into the runtime image, sets the entrypoint to node server.js). If you add it after the fact, regenerate the Dockerfile or hand-edit it to match.

The running app behaves identically to a default build. The one thing to watch: files you reference at runtime from outside .next (custom static files, locale data for next-intl, font files) need to be copied into the standalone directory at build time. Next.js auto-traces most of this, but custom paths can slip through. If you see ENOENT: no such file or directory at runtime, add the missing path to outputFileTracingIncludes in next.config.js.

Env vars: build-time vs runtime

Next.js has two flavors of environment variable, and Fly.io handles each differently.

NEXT_PUBLIC_* values are inlined into the JavaScript bundle during next build, so they have to be available at fly deploy time. Declare them under [build.args] in fly.toml and pass concrete values on each deploy:

[build]
[build.args]
  NEXT_PUBLIC_API_URL = ""
  NEXT_PUBLIC_SUPABASE_URL = ""
fly deploy \
  --build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
  --build-arg NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co

Don't put the actual values in fly.toml. The file is committed to source control; the values get baked into the public client bundle on every build anyway, but committing them couples your environments to your repo.

Server-only secrets (database URLs, OAuth client secrets, Stripe keys, JWT signing keys) belong in fly secrets set. They're injected as environment variables when each Machine boots and never make it into the JavaScript bundle:

fly secrets set DATABASE_URL=postgres://... STRIPE_SECRET_KEY=sk_live_...

Setting one or more secrets triggers a rolling restart. Read them server-side in route handlers, server components, or server actions via process.env.NAME.

The reason for two mechanisms: Next.js runs both at build time (collecting static prerendered pages, inlining client values) and at runtime (serving dynamic requests). Anything the browser needs has to be there at build time; anything only the server uses can wait until boot.

Make server actions work across multiple Machines

This is a Next.js-specific failure mode that bites every multi-Machine Fly.io deploy of an App Router app: server actions stop working after a deploy with errors like Failed to find Server Action.

Next.js encrypts server action closures with a key that gets generated per build and embedded into the bundle. Each Machine has its own build, so each Machine has its own key. When a user's browser invokes a server action from Machine A's bundle but the request lands on Machine B, Machine B can't decrypt the payload and rejects it.

The fix is to pin the key explicitly so every build uses the same value:

openssl rand -base64 32

The output of that command is your key. The catch: NEXT_SERVER_ACTIONS_ENCRYPTION_KEY has to be set at build time, not at runtime. It gets embedded into the build output and used automatically when the server starts. fly secrets set would put the value in the runtime environment but not in the build, so it wouldn't fix the problem. Use fly deploy --build-arg or declare it in fly.toml [build.args] instead:

[build]
[build.args]
  NEXT_SERVER_ACTIONS_ENCRYPTION_KEY = ""
fly deploy --build-arg NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=your-generated-key

Generate the value once, store it wherever you keep other build-time secrets (CI variable, password manager, a .env file that's not committed), and reuse it across every deploy. If you rotate the key, all previously-deployed bundles immediately stop accepting server actions, so coordinate the rotation with a full deploy.

Attach Managed Postgres without breaking the build

The recommended path is Fly.io Managed Postgres. MPG handles automatic backups and recovery, high availability with automatic failover, performance monitoring, resource scaling, and encryption of data at rest and in transit. We no longer support the older unmanaged fly postgres path; MPG is the supported way to run Postgres on Fly.io.

Provision a cluster and attach it:

fly mpg create
fly mpg list
fly mpg attach your-cluster-id -a your-app-name

fly mpg create walks through plan, region, and volume size. fly mpg list shows the cluster ID once it's ready. fly mpg attach writes a DATABASE_URL secret onto your app, reboots it, and uses a PGBouncer-pooled URL.

The Next.js-specific pitfall: the Fly.io remote builder doesn't have your app's secrets at build time, so any route that reads process.env.DATABASE_URL during static prerendering fails. Two ways to handle it.

Mark the route as dynamic so Next.js skips it at build time:

export const dynamic = 'force-dynamic'

Or use the connection() API to defer rendering to request time:

import { connection } from 'next/server'

export default async function Page() {
  await connection()
  const users = await db.query.users.findMany()
  return <UserList users={users} />
}

connection() tells Next.js the component depends on the request, so the renderer skips it at build time and runs it on each request. This is the cleaner pattern for App Router apps; force-dynamic is the heavier hammer.

Database access at runtime works fine; it's only the build phase that's sandboxed.

Run Next.js across regions

Next.js apps deploy in one region by default. To run in more, use fly scale count:

fly scale count 3 --region ord,fra,nrt

That places Machines in Chicago, Frankfurt, and Tokyo. Fly.io's proxy routes each incoming request to the nearest healthy Machine, so users in Europe hit Frankfurt, users in Asia hit Tokyo, and so on. Server components, server actions, and API routes all see shorter round-trip times, especially for chatty endpoints that the client calls multiple times per page load.

Two things to coordinate for multi-region.

First, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY has to be the same across every Machine's build. The previous section covers how to pin it.

Second, a Managed Postgres cluster lives in one region with a primary and a replica for in-region high availability. There are no cross-region read replicas; reads from Frankfurt back to a Chicago primary add latency you can't hide at the app layer.

The pragmatic move is to keep MPG in the region where most writes originate, run Next.js Machines globally in front of it, and cache hot read paths (Upstash Redis for sessions and rate limiting, Next.js fetch cache for upstream data, ISR for content) to absorb cross-region latency.

next/image and Tigris for global delivery

The next/image optimizer runs in the Node process by default, so it works on every Fly.io Machine with zero configuration. Each image hit goes through the running Next.js server, gets resized and re-encoded on the fly, and the optimized result is cached on the Machine's local disk.

For most apps this is fine. The optimizer is fast, the cached results are reusable, and a Machine that's serving images is also serving routes, so there's no extra infrastructure to manage.

For very high traffic or globally distributed media (user uploads, large image catalogs), point next/image at Tigris instead. Tigris is an S3-compatible object store with edge replication, available on Fly.io. Configure a custom loader in next.config.js to route image URLs through Tigris:

module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/tigris-loader.js',
  },
}

Your loader returns a Tigris URL with the requested width and quality. The optimization happens at the edge, your Machines stop spending CPU on image work, and image bytes get served from the closest Tigris point of presence.