Skip to content

Next.js Deployment & Self-Hosting

Deploying a Next.js App Router application requires an understanding of how server-rendered code, static assets, dynamic Route Handlers, and cache systems operate. Whether you are deploying to a serverless platform, containerizing for Kubernetes, or exporting a static website, Next.js provides optimized build pipelines for each strategy.


1. Deployment Strategies Overview

You can deploy Next.js using three primary models:

StrategyHosting PlatformRendering CapabilityBest For
Serverless/EdgeVercel, AWS AmplifySSR, SSG, ISR, Edge APITeams looking for zero-config global scaling and optimal CDN caching.
Self-Hosted (Containers)AWS, GCP, Azure, VPS (Docker)SSR, SSG, ISR, WebSocketsOrganizations with compliance regulations, custom firewalls, or existing Kubernetes setups.
Static Export (SSG)GitHub Pages, Netlify, S3SSG only (Static HTML)Static blogs, landing pages, or documentation sites that do not require server compute.

2. Deploying to Vercel (Serverless)

Vercel is the creator and maintainer of Next.js, providing the native environment for Next.js features (such as Edge middleware, ISR, and automatic image optimization).

How to Deploy

  1. Push your code to a Git provider (GitHub, GitLab, or Bitbucket).
  2. Import the project into the Vercel Dashboard.
  3. Vercel automatically detects Next.js, configures build commands (next build), and deploys it globally.

Managing Environment Variables on Vercel

Vercel allows you to set environment variables per environment (Production, Preview, Development) in the Project Settings. These variables are securely injected during the build and runtime stages.


3. Self-Hosting with Docker (Standalone Mode)

For self-hosting, Next.js can compile a minimized, isolated bundle containing only the code and dependencies required for production. This is called Standalone Mode and is ideal for Docker containers.

A. Enable Standalone Output

In your next.config.mjs (or next.config.js), enable the standalone build option:

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
export default nextConfig;

During next build, Next.js will use nft (Node File Trace) to create a folder at .next/standalone which copies only the active dependencies of your application, bypassing the bulky node_modules folder.

B. Write an Optimized Dockerfile

Use a multi-stage Dockerfile to keep the final production image size minimal (typically under 120MB instead of 1GB):

# 1. Base stage: Install dependencies
FROM node:18-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
RUN npm ci
# 2. Builder stage: Compile the application
FROM base AS builder
COPY . .
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# 3. Runner stage: Production execution
FROM node:18-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 public directory for static asset hosting
COPY --from=builder /app/public ./public
# Set permissions for caching on runtime
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Copy standalone build and static files
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"
# Run the server using the standalone Node.js file generated by Next.js
CMD ["node", "server.js"]

Build the container locally:

Terminal window
docker build -t my-next-app .
docker run -p 3000:3000 my-next-app

4. Environment Variables in Production

Understanding how environment variables behave is critical to avoiding leakages and misconfigurations.

Build-Time (Client-Side) Variables

Any environment variables prefixed with NEXT_PUBLIC_ are read during build time (next build) and baked directly into the JavaScript files sent to the client browser.

  • Rule: Changing a NEXT_PUBLIC_ variable requires rebuilt/redeployed containers. Never put database credentials or private keys in these variables.

Runtime (Server-Side) Variables

Variables without the prefix (e.g., DATABASE_URL, JWT_SECRET) are read at runtime on the server.

  • Rule: These can be injected dynamically in your Docker container or VPS environment without rebuilding the code.

5. Static Site Export (SSG)

If your app doesn’t require database connections or server computing, you can generate a purely static build consisting of HTML, CSS, and JS.

A. Enable Export Output

Configure Next.js to export static files:

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
};
export default nextConfig;

Running next build will output the static site to an out/ folder, which can be uploaded directly to static hosters like Netlify, GitHub Pages, or AWS S3.

[!WARNING] When output: "export" is enabled, server-only features are unsupported.

  • No dynamic Route Handlers (api/route.ts).
  • No Server Actions ("use server" mutations).
  • No Middleware (middleware.ts).
  • No dynamic Server Side Rendering (SSR) or request headers checks.

6. Incremental Static Regeneration (ISR) and Caching

Next.js automatically caches data fetches (fetch), static routes, and rendered HTML pages.

Self-Hosting Cache Challenges

In a serverless platform (like Vercel), caching is automatically shared globally on a CDN. In a self-hosted environment:

  • By default, Next.js writes cache files to the local file system (.next/cache).
  • If you scale your Docker container horizontally to run on multiple replicas (e.g., Kubernetes pods), the cache is not shared. Pod A will serve outdated page caches, while Pod B serves regenerated pages.

Solution: Centralized Cache Adapter

To resolve this, you can configure a custom cache handler to store caches in a shared Redis database:

cache-handler.js
const { Redis } = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
class CacheHandler {
constructor(options) {
this.options = options;
}
async get(key) {
const value = await redis.get(key);
if (!value) return null;
return JSON.parse(value);
}
async set(key, data, ctx) {
await redis.set(key, JSON.stringify(data));
}
async revalidateTag(tag) {
// Implement tag-based cache purging
}
}
module.exports = CacheHandler;

Register the handler in next.config.mjs:

next.config.mjs
export default {
cacheHandler: './cache-handler.js',
cacheMaxMemorySize: 0, // Disable memory cache to force Redis reliance
};

7. Production Best Practices

  1. Offload Static Assets: Configure a Content Delivery Network (like Cloudflare, AWS CloudFront) to cache /public and /_next/static assets, bypassing Node.js runtime load.
  2. Monitor Server Telemetry: Turn off Next.js analytics telemetry using ENV NEXT_TELEMETRY_DISABLED 1 in your Dockerfiles to avoid performance tracking overhead.
  3. Configure Nginx as a Reverse Proxy: Use Nginx or Caddy in front of your self-hosted Docker containers to handle SSL/TLS termination, rate limiting, and compression (Gzip/Brotli).
  4. Use Graceful Shutdown: Ensure your container process manager forwards the SIGTERM signal to Node.js, allowing active requests to complete before the container terminates.