All posts
nextjsdeployment

Deploying Next.js Apps: A Practical Guide for Full-Stack Developers

A practical guide to Deploying Next.js Apps — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

August 6, 2026

·
6 min read
·
0 views

Deploying Next.js apps is about choosing the right runtime for your data, not just picking a host. This guide covers the practical decisions that actually break production builds.

Deploying Next.js Apps is rarely a one-click operation once you move past a static portfolio. I've spent the last few years shipping Next.js applications to Vercel, AWS, and bare-metal servers, and the friction always comes from the same place: misunderstanding how the Node.js server, edge runtime, and static generation interact. Let me walk you through the real tradeoffs, with code you can actually run.

Why Deploying Next.js Apps Matters (and When to Skip It)

If your app is purely static—a blog, a marketing page, a docs site—you don't need a server at all. next build && next export gives you a folder of HTML files you can drop on any CDN. That's it. You're done in five minutes.

But the moment you add server actions, middleware, or API routes, you're committing to a long-running process. That's where the architecture decisions matter. I've seen teams burn days debugging fetch failures because they deployed a Node.js app to a serverless platform that only supported the edge runtime.

The rule I follow: if your data is fetched at request time, you need a Node.js server. If it's fetched at build time, you're static. Everything else is optimization.

Getting Started with Deploying Next.js Apps

Here's the minimal setup that works on any Node.js-compatible host—Vercel, Railway, Fly.io, or a VPS. First, configure your build script correctly in package.json:

{
  "scripts": {
    "build": "next build",
    "start": "next start -p $PORT"
  }
}

The $PORT environment variable is critical. Most platforms inject it at runtime, and hardcoding 3000 will crash your deployment. Here's a production-ready Dockerfile that handles this properly:

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Notice the output: 'standalone' setting in your next.config.ts—that's what makes the Docker build work efficiently:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'standalone',
};

export default nextConfig;

Core Deploying Next.js Apps Concepts Every Developer Should Know

1. Runtime Selection

Your code runs in one of three places: Node.js, the edge, or static at build time. Here's a practical way to think about it:

// This runs at build time — static generation
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}

// This runs at request time on the Node.js server
export async function GET(request: Request) {
  const data = await fetch('https://api.example.com/user-data', {
    cache: 'no-store', // always fresh
  });
  return Response.json(await data.json());
}

The edge runtime is great for middleware and simple auth checks, but it can't run heavy database drivers. If you're using Prisma or Sequelize, you need Node.js.

2. Caching Strategies

Next.js 14+ has a nuanced caching model. Here's the pattern I use for dynamic data that doesn't need real-time updates:

// Revalidate every 60 seconds, but serve stale immediately
export default async function Dashboard() {
  const data = await fetch('https://api.example.com/metrics', {
    next: { revalidate: 60 },
  });
  
  return <pre>{JSON.stringify(await data.json(), null, 2)}</pre>;
}

The key insight: revalidate: 60 means users get cached data for up to 60 seconds, then the server refreshes it in the background. This avoids cache stampedes and keeps response times low.

3. Environment Variables at Runtime

This is the one that bites everyone. Build-time variables are baked into your bundle. Runtime variables need the NEXT_PUBLIC_ prefix or server-side access:

// server-side only — safe for secrets
const dbUrl = process.env.DATABASE_URL;

// exposed to the browser — never put secrets here
const apiKey = process.env.NEXT_PUBLIC_ANALYTICS_KEY;

On most platforms, you'll set these in the dashboard. On Docker, pass them via -e flags or a .env file.

Common Deploying Next.js Apps Mistakes and How to Fix Them

Mistake 1: Forgetting to set output: 'standalone' — Your Docker image balloons to 1GB+ because it includes all of node_modules. Fix: add the config above and watch your image shrink to ~150MB.

Mistake 2: Using fetch without cache options — Next.js 14 defaults to no-store for fetch, which means every request hits your upstream API. If you're not caching, you're paying for serverless invocations you don't need. Fix: add next: { revalidate: 60 } or use cache: 'force-cache' for static data.

Mistake 3: Ignoring the edge runtime for middleware — I've seen people write heavy auth logic in middleware, causing cold starts on every request. Fix: keep middleware lean:

// middleware.ts — keep this file small
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session');
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

When Should You Use Deploying Next.js Apps?

You should use server-side deployment when you need any of these: server actions for form handling, API routes for internal logic, middleware for auth or redirects, or streaming responses. If your app is a CRUD interface with a database, you're deploying Next.js Apps. If it's a static site with a contact form, skip the server and use a form service.

The sweet spot is hybrid: statically generate your marketing pages, server-render your authenticated routes, and use ISR for content that updates periodically. That's the architecture I've used for production apps handling millions of requests.

Deploying Next.js Apps in Production

First, set up health checks. Most platforms need a GET /api/health endpoint to know your app is alive:

// app/api/health/route.ts
export async function GET() {
  return Response.json({ status: 'ok' });
}

Second, enable compression. Next.js doesn't compress responses by default in standalone mode. Add a custom server or use a reverse proxy like nginx with gzip enabled.

Third, monitor your cold starts. On serverless platforms, every new instance takes 1-2 seconds to boot. If you're seeing latency spikes, consider a warm pool or moving to a container platform with a minimum instance count.

One more thing: always test your Docker build locally before pushing. The standalone output has quirks—like needing public copied separately—that only show up in production.

Your takeaway: start with the standalone output and Dockerfile above, run it locally with docker build -t myapp . && docker run -p 3000:3000 myapp, and verify it works before touching any cloud platform. That single step will save you more debugging time than any other configuration choice you make today.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch