React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

deploymentdockerstandalonebuildedge-runtimevercelproduction

Deployment

Recipe

Deploy a Next.js 15+ App Router application using standalone output mode, Docker containers, edge runtime functions, and platform-specific optimizations.

Working Example

Standalone Build Configuration

// next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  output: "standalone",
};
 
export default nextConfig;
# Build the application
npm run build
 
# The standalone output is in .next/standalone
# Copy static and public assets
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
 
# Run the standalone server
node .next/standalone/server.js

Production Dockerfile

# Dockerfile
FROM node:20-alpine AS base
 
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
 
# Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
 
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
 
# Production image
FROM base AS runner
WORKDIR /app
 
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
 
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
 
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
 
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
 
CMD ["node", "server.js"]
# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - AUTH_SECRET=your-secret-here
    depends_on:
      - db
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
 
volumes:
  pgdata:

Edge Runtime Route

// app/api/geo/route.ts
import { NextRequest, NextResponse } from "next/server";
 
export const runtime = "edge";
 
export async function GET(request: NextRequest) {
  return NextResponse.json({
    city: request.geo?.city ?? "unknown",
    country: request.geo?.country ?? "unknown",
    region: request.geo?.region ?? "unknown",
  });
}

Edge Runtime Middleware

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
 
// Middleware always runs on the edge
export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? "US";
  const response = NextResponse.next();
  response.headers.set("x-user-country", country);
  return response;
}

Build Analysis Script

// package.json
{
  "scripts": {
    "build": "next build",
    "analyze": "ANALYZE=true next build",
    "start": "next start",
    "start:standalone": "node .next/standalone/server.js"
  }
}
// next.config.ts (with bundle analyzer)
import type { NextConfig } from "next";
import withBundleAnalyzer from "@next/bundle-analyzer";
 
const nextConfig: NextConfig = {
  output: "standalone",
};
 
export default withBundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
})(nextConfig);

Deep Dive

How It Works

  • output: "standalone" produces a self-contained build that includes only the necessary node_modules files. The output in .next/standalone can run with just node server.js, without needing the full node_modules directory.
  • Static assets (.next/static) and public/ are not included in the standalone output. They must be copied separately or served from a CDN.
  • The Edge Runtime uses a stripped-down V8 isolate (not Node.js). It starts faster and has lower memory overhead but cannot use Node.js APIs like fs, Buffer, or native modules.
  • Multi-stage Docker builds reduce the final image size by separating dependency installation, building, and the runtime image. A typical Next.js standalone Docker image is 100-200 MB.
  • HOSTNAME="0.0.0.0" is required in Docker to accept connections from outside the container. Without it, the server binds to 127.0.0.1 and is unreachable.

Variations

Static Export (No Server):

// next.config.ts
const nextConfig: NextConfig = {
  output: "export",
};

This generates a fully static site in the out/ directory. No Node.js server required. Limitations: no Server Components at request time, no API routes, no middleware, no ISR.

Vercel Deployment:

# Install Vercel CLI
npm i -g vercel
 
# Deploy (auto-detects Next.js)
vercel
 
# Deploy to production
vercel --prod

Vercel automatically handles standalone output, edge functions, and CDN distribution. No Dockerfile needed.

Custom Health Check Endpoint:

// app/api/health/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  return NextResponse.json({
    status: "ok",
    timestamp: new Date().toISOString(),
    version: process.env.APP_VERSION ?? "unknown",
  });
}

TypeScript Notes

  • request.geo is typed as { city?: string; country?: string; region?: string; latitude?: string; longitude?: string } | undefined. It is only populated on edge deployments (Vercel, Cloudflare).
  • The runtime export must be a string literal ("edge" or "nodejs"). It cannot be a variable.

Gotchas

  1. Standalone mode does not include public/ or .next/static/. You must copy these directories into the standalone output or serve them from a CDN.
  2. Edge Runtime does not support all Node.js APIs. fs, path, child_process, crypto (use globalThis.crypto), and most native modules are unavailable. Check the Next.js Edge Runtime API reference.
  3. Environment variables in Docker are baked in at build time. For runtime env vars, set them on the container (docker run -e KEY=value) and access them via process.env in server code. NEXT_PUBLIC_ vars are always build-time only.
  4. The standalone server does not serve static files. Configure a reverse proxy (nginx, Caddy) or CDN to serve /_next/static/ and /public/ paths.
  5. output: "export" disables all server features. No Server Components at request time, no Route Handlers, no middleware, no ISR. Only use for fully static sites.
  6. Image optimization requires Sharp in standalone mode. Add sharp to your dependencies: npm install sharp.

Alternatives

ApproachProsCons
VercelZero-config, edge-native, automatic CDNVendor lock-in, cost at scale
Docker + standalonePortable, any cloud providerManual infra setup, no edge by default
Static export (output: "export")No server, cheap hosting (S3, Cloudflare)No server features
AWS AmplifyManaged, supports SSRLimited edge config, AWS-specific
Cloudflare PagesEdge-first, fast, generous free tierLimited Node.js API support
Railway or Fly.ioDocker-based, simple DXSmaller ecosystem

FAQs

What does output: "standalone" do and why is it needed for Docker?
  • It produces a self-contained build that includes only the necessary node_modules files.
  • The output can run with just node server.js, without the full node_modules directory.
  • This dramatically reduces Docker image size (typically 100-200 MB).
Why must you copy .next/static and public/ separately in standalone mode?
  • The standalone output only includes server-side code and minimal dependencies.
  • Static assets and public files are excluded to allow serving them from a CDN.
  • You must copy them into the standalone directory or configure a reverse proxy to serve them.
What is HOSTNAME="0.0.0.0" and why is it required in Docker?
  • By default, the Next.js server binds to 127.0.0.1 (localhost only).
  • Inside a Docker container, 127.0.0.1 is unreachable from outside the container.
  • Setting HOSTNAME="0.0.0.0" makes the server accept connections from any network interface.
What is the Edge Runtime and what are its limitations?
  • The Edge Runtime uses V8 isolates (not Node.js) for faster cold starts and lower memory.
  • It cannot use Node.js APIs like fs, path, child_process, or Buffer.
  • Use globalThis.crypto instead of the crypto Node.js module.
Gotcha: Why do NEXT_PUBLIC_ variables not work as Docker runtime env vars?
  • NEXT_PUBLIC_ values are statically replaced at next build time.
  • Setting them as runtime env vars on the container has no effect.
  • You must rebuild the Docker image whenever a NEXT_PUBLIC_ value changes.
What does output: "export" produce and what features does it disable?
  • It generates a fully static site in the out/ directory with no server required.
  • It disables Server Components at request time, Route Handlers, middleware, and ISR.
  • Only use it for sites that are entirely static.
Why is Sharp needed in standalone mode for image optimization?
  • Next.js uses Sharp for on-the-fly image resizing and format conversion.
  • Standalone mode does not bundle Sharp automatically.
  • Add it explicitly with npm install sharp or images will fail to optimize.
How do you type the runtime export for an Edge Route Handler in TypeScript?
export const runtime = "edge";
// or
export const runtime = "nodejs";
  • The value must be a string literal, not a variable.
  • TypeScript will enforce this as "edge" | "nodejs".
What is the purpose of the multi-stage Docker build pattern shown on this page?
  • Stage 1 (deps) installs production dependencies only.
  • Stage 2 (builder) copies deps and builds the application.
  • Stage 3 (runner) copies only the standalone output, reducing the final image size.
  • Each stage discards build-time artifacts not needed in production.
Gotcha: Why does the standalone server not serve static files by default?
  • The standalone server.js is a minimal Node.js server focused on SSR.
  • Static file serving is expected to be handled by a CDN or reverse proxy (nginx, Caddy).
  • Serving static files through Node.js is less efficient than a dedicated static server.
How does request.geo work and when is it populated?
export async function GET(request: NextRequest) {
  const city = request.geo?.city ?? "unknown";
}
  • request.geo is only populated on edge deployments (Vercel, Cloudflare).
  • It is undefined in local development and non-edge Node.js deployments.
  • Always use optional chaining and provide fallback values.
What is the purpose of a health check endpoint in production deployments?
  • Load balancers and container orchestrators (Kubernetes, ECS) use it to verify the app is running.
  • It returns a simple { status: "ok" } response with optional metadata.
  • A failing health check triggers automatic container restarts or traffic rerouting.