Skip to main content
Technology & EngineeringNextjs381 lines

Deployment

Deployment strategies for Next.js including Vercel, self-hosting with Node.js, Docker containers, and static export

Quick Summary31 lines
You are an expert in deploying Next.js applications to production using Vercel, self-hosted Node.js servers, Docker containers, and static export.

## Key Points

- `.next/` — compiled application (server and client bundles)
- `.next/static/` — client-side assets (JS, CSS, media)
- `.next/server/` — server-rendered pages and API routes
- `.next/standalone/` — self-contained server (when `output: "standalone"`)
- Edge Functions: Middleware and Route Handlers with `export const runtime = "edge"`
- Image Optimization: Handled automatically at the CDN edge
- ISR: Managed cache with on-demand revalidation across all regions
- Analytics: Web Vitals and Speed Insights built-in
- Preview Deployments: Every pull request gets a unique URL
- No Server Components (everything is pre-rendered at build time)
- No Route Handlers
- No Middleware

## Quick Example

```bash
next build
```

```bash
npm run build
node .next/standalone/server.js
# or, without standalone:
npm start  # runs "next start"
```
skilldb get nextjs-skills/DeploymentFull skill: 381 lines
Paste into your CLAUDE.md or agent config

Deployment — Next.js

You are an expert in deploying Next.js applications to production using Vercel, self-hosted Node.js servers, Docker containers, and static export.

Overview

Next.js supports multiple deployment targets. Vercel (the creators of Next.js) offers the most streamlined experience with zero-configuration deployments. For teams requiring infrastructure control, Next.js can be self-hosted as a Node.js server, containerized with Docker, or exported as a fully static site.

Core Concepts

Build Output

next build

This produces:

  • .next/ — compiled application (server and client bundles)
  • .next/static/ — client-side assets (JS, CSS, media)
  • .next/server/ — server-rendered pages and API routes
  • .next/standalone/ — self-contained server (when output: "standalone")

Output Modes

ModeConfigUse Case
Default(none)Vercel or full Node.js server
Standaloneoutput: "standalone"Docker, self-hosted
Staticoutput: "export"CDN-only, no server needed

Core Philosophy

Deployment strategy should follow from your operational constraints, not the other way around. Vercel offers the path of least resistance — zero-config deploys, managed infrastructure, preview environments per pull request — and is the right choice when you want to focus entirely on product development. But when compliance, cost, data residency, or existing infrastructure demand it, Next.js is designed to run anywhere Node.js runs, without vendor lock-in.

The output: "standalone" mode embodies the principle of minimal deployment artifacts. Instead of shipping your entire node_modules directory into a container, the standalone build traces your imports and produces a self-contained server with only the code it actually needs. This dramatically reduces image sizes, speeds up cold starts, and limits your attack surface. It is the foundation of any serious Docker or Kubernetes deployment.

Environment management is a discipline, not an afterthought. The split between build-time variables (NEXT_PUBLIC_) and runtime variables (server-only) is a security boundary: anything prefixed with NEXT_PUBLIC_ is permanently embedded in the client JavaScript bundle. Treat that prefix as a publication decision. Secrets, API keys, and database URLs must never carry the prefix, and .env.local must always be in .gitignore.

Implementation Patterns

Deploying to Vercel

Vercel requires no special configuration. Connect your Git repository and it detects Next.js automatically.

Environment variables: Set them in the Vercel dashboard under Project Settings > Environment Variables.

Custom build settings (optional):

// vercel.json
{
  "buildCommand": "prisma generate && next build",
  "installCommand": "npm ci",
  "framework": "nextjs",
  "regions": ["iad1"],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "no-store" }
      ]
    }
  ],
  "redirects": [
    {
      "source": "/old-blog/:slug",
      "destination": "/blog/:slug",
      "permanent": true
    }
  ]
}

Vercel-specific features:

  • Edge Functions: Middleware and Route Handlers with export const runtime = "edge"
  • Image Optimization: Handled automatically at the CDN edge
  • ISR: Managed cache with on-demand revalidation across all regions
  • Analytics: Web Vitals and Speed Insights built-in
  • Preview Deployments: Every pull request gets a unique URL

Self-Hosting with Node.js

Build and run:

npm run build
node .next/standalone/server.js
# or, without standalone:
npm start  # runs "next start"

Production process manager (PM2):

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "nextjs-app",
      script: ".next/standalone/server.js",
      instances: "max",
      exec_mode: "cluster",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
        HOSTNAME: "0.0.0.0",
      },
    },
  ],
};
pm2 start ecosystem.config.js
pm2 save
pm2 startup

Docker Deployment

next.config.ts:

const nextConfig: NextConfig = {
  output: "standalone",
};
export default nextConfig;

Multi-stage Dockerfile:

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts

# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# Stage 3: Production runner
FROM node:20-alpine 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 standalone build
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

.dockerignore:

node_modules
.next
.git
*.md
.env*.local

Build and run:

docker build -t nextjs-app .
docker run -p 3000:3000 --env-file .env.production nextjs-app

Docker Compose for full stack:

# docker-compose.yml
version: "3.9"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Static Export

For fully static sites (no server required):

// next.config.ts
const nextConfig: NextConfig = {
  output: "export",
  // Optional: trailing slashes for static hosting
  trailingSlash: true,
  // Optional: custom base path
  // basePath: "/my-app",
};
next build
# Output is in the "out/" directory
# Deploy to any static host: S3, CloudFlare Pages, GitHub Pages, Netlify

Limitations of static export:

  • No Server Components (everything is pre-rendered at build time)
  • No Route Handlers
  • No Middleware
  • No ISR or on-demand revalidation
  • No next/image optimization (use a custom loader or unoptimized: true)
  • No dynamic routes without generateStaticParams

Reverse Proxy with Nginx

# /etc/nginx/sites-available/nextjs
upstream nextjs {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Static assets — serve directly from Nginx
    location /_next/static {
        alias /var/www/nextjs/.next/static;
        expires 365d;
        add_header Cache-Control "public, immutable";
    }

    location /public {
        alias /var/www/nextjs/public;
        expires 30d;
    }

    # Proxy everything else to Next.js
    location / {
        proxy_pass http://nextjs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Environment Variables

# .env.local (development — git-ignored)
DATABASE_URL=postgresql://localhost:5432/dev

# .env.production (production defaults — committed)
NEXT_PUBLIC_APP_URL=https://example.com

# Runtime variables (set in hosting platform)
# DATABASE_URL, API_KEY, AUTH_SECRET, etc.

Key rules:

  • NEXT_PUBLIC_ prefix exposes variables to the browser bundle.
  • Server-only variables (no prefix) are available only in Server Components, Route Handlers, and Server Actions.
  • .env.local overrides all other env files and should be in .gitignore.

Health Check Endpoint

// app/api/health/route.ts
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET() {
  try {
    // Check database connectivity
    await db.$queryRaw`SELECT 1`;

    return NextResponse.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
    });
  } catch {
    return NextResponse.json(
      { status: "unhealthy" },
      { status: 503 }
    );
  }
}

Best Practices

  • Use output: "standalone" for Docker and self-hosted deployments — it produces a minimal server with only the required node_modules.
  • Set HOSTNAME="0.0.0.0" in container environments so the server listens on all interfaces.
  • Serve _next/static/ directly from a CDN or reverse proxy with long cache headers — these files are content-hashed and immutable.
  • Use a health check endpoint for orchestrators (Kubernetes, ECS, Docker Compose) to monitor application readiness.
  • Keep secrets out of NEXT_PUBLIC_ variables — anything with that prefix is embedded in the client JavaScript bundle.
  • Use preview deployments (Vercel) or staging environments to validate changes before production.

Common Pitfalls

  • Missing standalone output: Without output: "standalone", the Docker image must include the entire node_modules directory, resulting in a much larger image.
  • Forgetting to copy public/ and .next/static/: The standalone output does not include these directories. They must be copied separately in the Dockerfile.
  • HOSTNAME not set in Docker: By default, next start binds to localhost, which is unreachable from outside the container. Set HOSTNAME=0.0.0.0.
  • Static export with dynamic features: Using output: "export" with features that require a server (middleware, Route Handlers, ISR) causes build errors or silent failures.
  • Environment variables at build vs runtime: NEXT_PUBLIC_ variables are inlined at build time. Changing them requires a rebuild. Server-only variables are read at runtime.
  • Image optimization without a server: next/image requires a running server for on-demand optimization. For static export, use images: { unoptimized: true } or a custom CDN loader.

Anti-Patterns

  • Deploying without output: "standalone" in Docker. Without it, your container must include the full node_modules directory, ballooning image size from ~100MB to 500MB+ and dramatically slowing cold starts. Always enable standalone mode for containerized deployments.

  • Hardcoding environment-specific values in next.config.ts. API URLs, feature flags, and service endpoints that differ between staging and production belong in environment variables, not config files. Hardcoding them forces a rebuild for every environment and makes rollbacks painful.

  • Exposing secrets via NEXT_PUBLIC_ prefix. Any variable with this prefix is inlined into the client JavaScript bundle at build time and is visible to anyone who opens browser DevTools. Database URLs, API keys, and auth secrets must use server-only variables with no prefix.

  • Skipping health check endpoints in orchestrated environments. Kubernetes, ECS, and Docker Compose need a way to know your application is ready. Without a /api/health endpoint that verifies database connectivity, your orchestrator cannot distinguish between a healthy instance and one that started but cannot serve requests.

  • Using static export (output: "export") and expecting server features. Static export produces a directory of HTML files with no running server. Middleware, Route Handlers, ISR, and next/image optimization all require a server. Choosing static export for a project that needs any of these features leads to silent failures and confusing production behavior.

Install this skill directly: skilldb add nextjs-skills

Get CLI access →