Next.js apps don’t just run; they react.
Let’s get your Next.js app humming on Fly.io. Forget manual server setup; Fly.io handles the infrastructure, letting you focus on your code. We’re talking about a full-stack JavaScript app running on a global edge network.
Here’s a quick look at what a Fly.io deployment looks like under the hood for a Next.js app. Imagine a request comes in. Fly.io’s edge network receives it and routes it to the closest available VM running your Next.js app. Your app, likely using Node.js under the hood, spins up its server (often a custom server built by Next.js or by you) and renders the page. If it’s a dynamic route or API call, your app fetches data and returns the response. Fly.io then sends that response back through the edge network to the user. It’s fast, distributed, and surprisingly simple once you’ve got the pieces in place.
The Core Components
- Your Next.js App: This is the heart of it all. It needs to be built for production.
fly.toml: This is Fly.io’s configuration file. It tells Fly.io how to run your app – what image to use, how to expose ports, resource limits, etc.Dockerfile: This describes how to build a container image for your Next.js app. Fly.io uses this to package your application and its dependencies.package.json: Standard Node.js dependency management, but crucially, it needs abuildscript and astartscript.
Step-by-Step Deployment
First, ensure you have the Fly.io CLI installed and are logged in.
fly auth login
Now, navigate to your Next.js project’s root directory.
1. Prepare Your Next.js App
Make sure your package.json has the necessary scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start -p $PORT",
"lint": "next lint"
}
}
The -p $PORT in the start script is crucial. Fly.io injects the $PORT environment variable, telling your Node.js server which port to listen on.
2. Create a Dockerfile
This Dockerfile is optimized for Next.js. It uses a multi-stage build to keep the final image lean.
# syntax=docker/dockerfile:1
# ---- Base ----
FROM oven/bun:latest AS base
# You can also use nodejs or node with a specific version, e.g., FROM node:18-alpine
# Using Bun for potentially faster builds and smaller images, but Node.js is fully supported.
# ---- Builder ----
FROM base AS builder
WORKDIR /app
# Install dependencies using Bun (or npm/yarn if you prefer)
COPY package*.json .
RUN bun install --frozen-lockfile
# Copy the rest of your application code
COPY . .
# Build the Next.js application
RUN bun run build
# ---- Runner ----
FROM base AS runner
WORKDIR /app
# Copy built application from builder stage
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public
# Set the port environment variable (Fly.io injects this)
ENV PORT 8080
# Expose the port your app will listen on
EXPOSE ${PORT}
# Command to run the application
CMD ["bun", "run", "start"]
Why this Dockerfile works:
- Multi-stage build: Reduces the final image size by not including build tools.
- Dependency caching: Copies
package.jsonand installs dependencies before copying the rest of the code, leveraging Docker’s layer caching. bun install --frozen-lockfile(ornpm ci): Ensures reproducible builds by using your lock file.bun run build: Executes the Next.js build command.ENV PORT 8080andEXPOSE ${PORT}: Standard practice for containerized Node.js apps. Fly.io will overridePORTwith its assigned value.CMD ["bun", "run", "start"]: The command to launch your Next.js production server.
3. Create fly.toml
In the root of your project, create a file named fly.toml:
app = "your-nextjs-app-name" # Replace with your desired app name on Fly.io
primary_region = "ord" # Or your preferred region, e.g., "lhr", "sin"
[build]
dockerfile = "Dockerfile" # Point to your Dockerfile
[experimental]
auto_assign_assets = true
cmd = "bun run start" # This is often redundant if your Dockerfile has a CMD, but good to have
[http_service]
internal_port = 8080 # Must match the EXPOSE port in your Dockerfile
force_https = true
auto_stop_machines = true
auto_deploy = true
[[http_service.services]]
protocol = "tcp"
port = 80
Why this fly.toml is important:
app: Your unique identifier on Fly.io.primary_region: Where your app’s primary instance will live.[build]: Tells Fly.io to use Docker and specifies theDockerfileto use.[http_service]: Configures how your app handles HTTP traffic.internal_port = 8080: This must match the port your application listens on inside the container (as defined by$PORTandEXPOSEin yourDockerfile).force_https = true: Automatically redirects HTTP to HTTPS.auto_stop_machines = true: If your app has no traffic for a while, Fly.io will stop its machines to save costs.auto_deploy = true: Automatically deploys new versions when you push code.
[[http_service.services]]: Defines the public-facing port.port = 80means it listens for standard HTTP requests. Fly.io handles TLS termination, so you don’t need to manage certificates.
4. Deploy
Now, run the deploy command from your project’s root:
fly deploy
Fly.io will build your Docker image, push it to its registry, and then deploy it to a VM. This might take a few minutes.
5. Access Your App
Once the deployment is complete, Fly.io will give you a URL. You can also find it with:
fly apps show
The output will include your app’s URL, typically something like your-nextjs-app-name.fly.dev.
Handling Environment Variables
For sensitive information or configuration specific to your production environment (like API keys), use flyctl secrets:
fly secrets set MY_API_KEY=your_secret_key --app your-nextjs-app-name
Your Next.js app can access these secrets as environment variables. In next.config.js or directly in your server code, you’d reference process.env.MY_API_KEY.
The Next Step: Performance and Edge Functions
Once your app is deployed, you’ll likely want to optimize it further. Consider exploring Fly.io’s Edge Functions for tasks like A/B testing, personalization, or request rewriting directly at the edge, before they even hit your Next.js application.