Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Deploy a Next.js 15+ App Router application using standalone output mode, Docker containers, edge runtime functions, and platform-specific optimizations.
// 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# 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:// 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",
});
}// 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;
}// 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);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..next/static) and public/ are not included in the standalone output. They must be copied separately or served from a CDN.fs, Buffer, or native modules.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.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",
});
}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).runtime export must be a string literal ("edge" or "nodejs"). It cannot be a variable.public/ or .next/static/. You must copy these directories into the standalone output or serve them from a CDN.fs, path, child_process, crypto (use globalThis.crypto), and most native modules are unavailable. Check the Next.js Edge Runtime API reference.docker run -e KEY=value) and access them via process.env in server code. NEXT_PUBLIC_ vars are always build-time only./_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.sharp to your dependencies: npm install sharp.| 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 |
node_modules files.node server.js, without the full node_modules directory.127.0.0.1 (localhost only).127.0.0.1 is unreachable from outside the container.HOSTNAME="0.0.0.0" makes the server accept connections from any network interface.fs, path, child_process, or Buffer.globalThis.crypto instead of the crypto Node.js module.NEXT_PUBLIC_ values are statically replaced at next build time.NEXT_PUBLIC_ value changes.out/ directory with no server required.npm install sharp or images will fail to optimize.export const runtime = "edge";
// or
export const runtime = "nodejs";"edge" | "nodejs".deps) installs production dependencies only.builder) copies deps and builds the application.runner) copies only the standalone output, reducing the final image size.server.js is a minimal Node.js server focused on SSR.export async function GET(request: NextRequest) {
const city = request.geo?.city ?? "unknown";
}request.geo is only populated on edge deployments (Vercel, Cloudflare).undefined in local development and non-edge Node.js deployments.{ status: "ok" } response with optional metadata.Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥