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.jsProduction 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 necessarynode_modulesfiles. The output in.next/standalonecan run with justnode server.js, without needing the fullnode_modulesdirectory.- Static assets (
.next/static) andpublic/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 to127.0.0.1and 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 --prodVercel 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.geois typed as{ city?: string; country?: string; region?: string; latitude?: string; longitude?: string } | undefined. It is only populated on edge deployments (Vercel, Cloudflare).- The
runtimeexport must be a string literal ("edge"or"nodejs"). It cannot be a variable.
Gotchas
- Standalone mode does not include
public/or.next/static/. You must copy these directories into the standalone output or serve them from a CDN. - Edge Runtime does not support all Node.js APIs.
fs,path,child_process,crypto(useglobalThis.crypto), and most native modules are unavailable. Check the Next.js Edge Runtime API reference. - 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 viaprocess.envin server code.NEXT_PUBLIC_vars are always build-time only. - The standalone server does not serve static files. Configure a reverse proxy (nginx, Caddy) or CDN to serve
/_next/static/and/public/paths. 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.- Image optimization requires Sharp in standalone mode. Add
sharpto your dependencies:npm install sharp.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Vercel | Zero-config, edge-native, automatic CDN | Vendor lock-in, cost at scale |
| Docker + standalone | Portable, any cloud provider | Manual infra setup, no edge by default |
Static export (output: "export") | No server, cheap hosting (S3, Cloudflare) | No server features |
| AWS Amplify | Managed, supports SSR | Limited edge config, AWS-specific |
| Cloudflare Pages | Edge-first, fast, generous free tier | Limited Node.js API support |
| Railway or Fly.io | Docker-based, simple DX | Smaller 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_modulesfiles. - The output can run with just
node server.js, without the fullnode_modulesdirectory. - 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.1is 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, orBuffer. - Use
globalThis.cryptoinstead of thecryptoNode.js module.
Gotcha: Why do NEXT_PUBLIC_ variables not work as Docker runtime env vars?
NEXT_PUBLIC_values are statically replaced atnext buildtime.- 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 sharpor 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.jsis 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.geois only populated on edge deployments (Vercel, Cloudflare).- It is
undefinedin 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.
Related
- Environment Variables - configuring env vars for production
- SEO - metadata and sitemaps for production sites
- Error Handling - production error monitoring
- Next.js Deployment Docs