Deployment
Deployment strategies for Next.js including Vercel, self-hosting with Node.js, Docker containers, and static export
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 linesDeployment — 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 (whenoutput: "standalone")
Output Modes
| Mode | Config | Use Case |
|---|---|---|
| Default | (none) | Vercel or full Node.js server |
| Standalone | output: "standalone" | Docker, self-hosted |
| Static | output: "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/imageoptimization (use a custom loader orunoptimized: 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.localoverrides 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 requirednode_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
standaloneoutput: Withoutoutput: "standalone", the Docker image must include the entirenode_modulesdirectory, 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. HOSTNAMEnot set in Docker: By default,next startbinds tolocalhost, which is unreachable from outside the container. SetHOSTNAME=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/imagerequires a running server for on-demand optimization. For static export, useimages: { unoptimized: true }or a custom CDN loader.
Anti-Patterns
-
Deploying without
output: "standalone"in Docker. Without it, your container must include the fullnode_modulesdirectory, 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/healthendpoint 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, andnext/imageoptimization 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
Related Skills
API Routes
Route Handlers for building REST APIs, handling webhooks, streaming responses, and CORS in Next.js App Router
App Router
App Router fundamentals including file-based routing, layouts, loading states, and parallel routes in Next.js
Authentication
Authentication patterns using NextAuth.js (Auth.js) and Clerk for protecting routes and managing sessions in Next.js
Data Fetching
Data fetching and caching strategies including Server Components, fetch options, ISR, and the Next.js cache layers
Image Optimization
Image optimization with next/image, asset handling, fonts, and performance tuning for Next.js applications
Middleware
Middleware patterns for request interception, redirects, rewrites, authentication guards, and geo-routing in Next.js