Next.js hosting on Fly.io:
a full-stack React framework, deployed with one command
Fly.io is a developer-centric public cloud with everything you need to deploy your Next.js app at scale.
Launch NowReady, 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.
> Install flyctl on Windows
PS C:> powershell -Command "iwr https://fly.io/install.ps1 -useb | iex"
> Run from Next.js project root
PS C:> fly launch
> Scale CPU, memory, instances & regions
PS C:> fly scale
Not on Windows? Install flyctl for your platform.
Releases That Don't Drop Requests
fly deploy builds a new image and does a rolling update of your Machines by default. Set strategy = "bluegreen" under [deploy] in fly.toml and a new set of Machines boots next to the running ones, with traffic moving over only after their health checks pass.
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.
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)
- Automatic encryption of data at rest and in transit
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.
-
S3-Compatible Object Storage
Tigris is S3-compatible object storage run on Fly.io. fly storage create makes a bucket and sets its credentials as secrets on your app, so your code reads them from the environment. Zero egress costs.
-
Managed Data Services for Every Occasion
Supabase for Postgres. LiteFS for SQLite. Upstash for Redis®. Fly.io integrates with these managed data services, so you don't run them yourself.
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, suggests a region, 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 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.
What you didn't have to set up
Look back at what actually happened. You ran fly launch, then fly mpg create and fly mpg attach. A standard Next.js app with a Postgres database in one region is the ordinary case Fly.io is built for, not a stretch. Here is the work that never appeared:
A hosting service for your code. fly launch and fly deploy build from the directory you are standing in. A Next.js project with no git remote at all deploys fine.
A Dockerfile. fly launch detects Next.js and writes one, along with the fly.toml. Both land in your repository where you can read them, edit them, and see them in a diff.
A connection string. fly mpg attach writes DATABASE_URL onto the app as a secret. It is there for your server components and route handlers at runtime, and the password never lands in your clipboard or your shell history.
A migration step in a deploy pipeline. fly launch writes release_command into fly.toml, so your migration command runs on a throwaway Machine before any Machine takes traffic, whether that is prisma migrate deploy, drizzle-kit migrate, or something else. A migration that fails fails the deploy, instead of half migrating production.
TLS and a hostname. The app answers on <app>.fly.dev with HTTPS from the first request, and the certificate is issued and renewed for you.
A load balancer or a VPC. Fly Proxy is already in front of your Machines, and your app reaches Postgres over your organization's private network as soon as the cluster is attached.
None of that work is missing. It is done, and the record of it is a fly.toml you can read in one screen, diff in a pull request, and revert with git.
next/image and Tigris object storage
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 large media sets (user uploads, large image catalogs), point next/image at Tigris instead. Tigris is S3-compatible object storage run on Fly.io, and fly storage create sets the bucket credentials as app secrets. 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. Your Machines stop spending CPU on image work, and Tigris serves the image bytes instead of your app.