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:
| Strategy | Hosting Platform | Rendering Capability | Best For |
|---|---|---|---|
| Serverless/Edge | Vercel, AWS Amplify | SSR, SSG, ISR, Edge API | Teams looking for zero-config global scaling and optimal CDN caching. |
| Self-Hosted (Containers) | AWS, GCP, Azure, VPS (Docker) | SSR, SSG, ISR, WebSockets | Organizations with compliance regulations, custom firewalls, or existing Kubernetes setups. |
| Static Export (SSG) | GitHub Pages, Netlify, S3 | SSG 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
- Push your code to a Git provider (GitHub, GitLab, or Bitbucket).
- Import the project into the Vercel Dashboard.
- 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:
/** @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 dependenciesFROM node:18-alpine AS baseWORKDIR /appRUN apk add --no-cache libc6-compatCOPY package.json package-lock.json ./RUN npm ci
# 2. Builder stage: Compile the applicationFROM base AS builderCOPY . .# Disable telemetry during buildENV NEXT_TELEMETRY_DISABLED 1RUN npm run build
# 3. Runner stage: Production executionFROM node:18-alpine AS runnerWORKDIR /app
ENV NODE_ENV productionENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjs
# Copy public directory for static asset hostingCOPY --from=builder /app/public ./public
# Set permissions for caching on runtimeRUN mkdir .nextRUN chown nextjs:nodejs .next
# Copy standalone build and static filesCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000ENV PORT 3000ENV HOSTNAME "0.0.0.0"
# Run the server using the standalone Node.js file generated by Next.jsCMD ["node", "server.js"]Build the container locally:
docker build -t my-next-app .docker run -p 3000:3000 my-next-app4. 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:
/** @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:
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:
export default { cacheHandler: './cache-handler.js', cacheMaxMemorySize: 0, // Disable memory cache to force Redis reliance};7. Production Best Practices
- Offload Static Assets: Configure a Content Delivery Network (like Cloudflare, AWS CloudFront) to cache
/publicand/_next/staticassets, bypassing Node.js runtime load. - Monitor Server Telemetry: Turn off Next.js analytics telemetry using
ENV NEXT_TELEMETRY_DISABLED 1in your Dockerfiles to avoid performance tracking overhead. - 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).
- Use Graceful Shutdown: Ensure your container process manager forwards the
SIGTERMsignal to Node.js, allowing active requests to complete before the container terminates.