Next.js 17 ▲ Deploying — Vercel deployment & self-hosting prerequisites
Next.js has two deployment paths that share the same build output but differ in what the platform provides. Vercel is the managed path: it detects the framework, configures the build, provisions the CDN and serverless functions, and handles the caching layers automatically. Self-hosting is the do-it-yourself path: the application is built into a standalone directory, copied into a container or a bare-metal host, and served behind a reverse proxy, with the caching, image optimization, and multi-instance coordination configured by the operator. The build artifact is the same; the operational surface is not.
This chapter covers the Vercel deployment model, the self-hosting prerequisites that a production deployment must satisfy, and the configuration options that determine whether a self-hosted deployment behaves like the managed one or diverges from it.
Key point: Self-hosting requires output: 'standalone' in next.config.ts, a Node.js server behind a reverse proxy, and a shared cache handler if more than one instance runs. Without the shared cache handler, ISR and revalidateTag produce inconsistent content across instances.
Why the two paths diverge
The build output problem. Vercel builds the application and deploys the result to its own infrastructure. The build output is not something the developer copies anywhere; Vercel’s build system reads the framework preset, runs next build, and serves the output from its CDN and functions. A self-hosted deployment must produce a portable artifact. The output: 'standalone' option creates that artifact: a minimal .next/standalone directory containing the server entry point (server.js), the production node_modules, and the build output, with the public/ and .next/static/ directories copied separately .
The caching problem. On Vercel, ISR and the Data Cache are shared across the deployment’s infrastructure. A revalidation on one edge node is visible to the others. On a self-hosted single instance, the default filesystem cache works because there is only one filesystem. On multiple instances behind a load balancer, each instance has its own cache, and a revalidation on one instance is invisible to the others. The documentation states this directly: “If you are hosting Next.js using a container orchestration platform like Kubernetes, each pod will have a copy of the cache. To prevent stale data from being shown since the cache is not shared between pods by default, you can configure the Next.js cache to provide a cache handler and disable in-memory caching” .
The encryption problem. Next.js encrypts Server Function closure variables before sending them to the client. By default, a unique encryption key is generated for each build. When running multiple server instances, all instances must use the same key, or a Server Function encrypted by one instance cannot be decrypted by another, producing “Failed to find Server Action” errors. The NEXT_SERVER_ACTIONS_ENCRYPTION_KEY environment variable sets a consistent key across instances .
a. Vercel deployment
The Vercel path is a Git integration. The repository is connected to a Vercel project, and every push to the production branch triggers a build. Vercel detects the Next.js framework, configures the build command (next build), and sets the output directory automatically . The framework preset handles the routing, the CDN, and the serverless functions without manual configuration.
A Vercel project requires Node.js 20.19 or later and a Vercel account. The deployment is triggered from the dashboard or the Vercel CLI. The vercel CLI can be installed globally and used to link a local project to a Vercel project and deploy it .
The platform provides features that a self-hosted deployment must replicate or forgo. Middleware runs globally on the Vercel edge network, with access to geolocation helpers and the NextRequest and NextResponse objects . Draft Mode is secured behind Vercel’s authentication, and the CDN bypasses the ISR cache automatically when Draft Mode is active . The OG image generation library (@vercel/og for the Pages Router, next/og for the App Router) is available without installation, and generated images are cached on the Vercel CDN .
The build configuration on Vercel can be overridden per project or per deployment. The buildCommand and outputDirectory fields in vercel.json override the framework defaults, and the framework field overrides the preset . For most Next.js projects, the defaults are correct, and the overrides exist for monorepos and custom build pipelines.
b. Self-hosting prerequisites
The first prerequisite is the standalone output. The output: 'standalone' option in next.config.ts produces the minimal server bundle. The .next/standalone directory contains the server entry point and the production dependencies, but it does not contain the public/ directory or the .next/static/ directory. Those must be copied into the standalone directory before the server starts .
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
}
export default nextConfig
The second prerequisite is the runtime. The standalone server runs on Node.js. A Dockerfile uses a Node base image, copies the standalone output, copies the static assets, and runs node server.js with NODE_ENV=production and PORT and HOSTNAME set . The server binds to the port, and a reverse proxy (Nginx, Caddy, or a cloud load balancer) terminates TLS and forwards traffic to it.
The third prerequisite is the reverse proxy. The proxy handles TLS termination, compression, and the static asset caching headers. The Next.js documentation notes that immutable assets are served with Cache-Control: public, max-age=31536000, immutable, and the proxy should respect that header. ISR responses carry s-maxage and stale-while-revalidate, and the CDN or proxy must respect those directives for the caching to work at the edge .
The fourth prerequisite is the shared cache handler if more than one instance runs. The handler implements get, set, revalidateTag, and resetRequestCache, and it stores entries in a location that every instance can reach, such as Redis or S3. The entries must be serialized, because they can contain Buffer and Map values that JSON cannot round-trip. The entries must be indexed by tag, so that a revalidateTag call on one instance invalidates the entry on every instance. The cacheMaxMemorySize: 0 option disables the default in-memory cache so that the handler owns the caching behavior .
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0,
}
The fifth prerequisite is the Server Actions encryption key. A multi-instance deployment sets NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to a consistent base64-encoded key. A single-instance deployment does not need it, because the key generated at build time is used by the single instance .
The sixth prerequisite is the build ID. When rebuilding for each stage of an environment and booting multiple containers, the build ID must be consistent. The generateBuildId option in next.config.js sets a consistent ID, such as the Git commit hash .
Complete Example Session
// ============================================
// PART 1: THE STANDALONE OUTPUT
// ============================================
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
}
export default nextConfig
// The build produces .next/standalone/server.js.
# ============================================
# PART 2: THE DOCKERFILE
# ============================================
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
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"]
# ============================================
# PART 3: THE REVERSE PROXY
# ============================================
upstream nextjs_app {
server 127.0.0.1:3000;
}
server {
listen 443 ssl http2;
server_name example.com;
location / {
proxy_pass http://nextjs_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /_next/static {
alias /app/.next/static;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
// ============================================
// PART 4: THE CACHE HANDLER
// ============================================
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0,
}
// ============================================
// PART 5: THE CACHE HANDLER IMPLEMENTATION
// ============================================
const cache = new Map()
module.exports = class CacheHandler {
async get(key) {
return cache.get(key)
}
async set(key, data, ctx) {
cache.set(key, { value: data, lastModified: Date.now(), tags: ctx.tags })
}
async revalidateTag(tags) {
tags = [tags].flat()
for (const [key, value] of cache) {
if (value.tags.some((tag) => tags.includes(tag))) {
cache.delete(key)
}
}
}
resetRequestCache() {}
}
# ============================================
# PART 6: THE ENCRYPTION KEY
# ============================================
# For multi-instance deployments:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=base64-encoded-key
# The key must be the same on every instance.
// ============================================
// PART 7: THE BUILD ID
// ============================================
module.exports = {
generateBuildId: async () => {
return process.env.GIT_HASH
},
}
// Consistent across containers.
# ============================================
# PART 8: THE BUILD AND RUN
# ============================================
npm run build
docker build -t myapp .
docker run -p 3000:3000 -e NODE_ENV=production myapp
# ============================================
# PART 9: THE VERCEL DEPLOYMENT
# ============================================
npm i -g vercel
vercel
# Follow the prompts to link and deploy.
// ============================================
// PART 10: THE VERCEL OVERRIDE
// ============================================
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": ".next"
}
The ten parts covered the standalone output, the Dockerfile, the reverse proxy, the cache handler configuration, the cache handler implementation, the encryption key, the build ID, the build and run, the Vercel deployment, and the Vercel override.
Quick Reference
Vercel vs self-hosting
| Concern | Vercel | Self-hosting |
|---|---|---|
| Build | Auto-detected | Manual next build |
| Output | Managed | output: 'standalone' |
| CDN | Built-in | Reverse proxy / CDN |
| ISR cache | Shared | Shared only with handler |
| Server Functions key | Managed | NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
| Image optimization | Built-in | Built-in or external loader |
Self-hosting prerequisites
| Prerequisite | Detail |
|---|---|
| Standalone output | output: 'standalone' |
| Runtime | Node.js, node server.js |
| Reverse proxy | TLS termination, static caching |
| Shared cache handler | For multi-instance ISR |
| Encryption key | For multi-instance Server Functions |
| Build ID | For multi-container consistency |
Cache handler requirements
| Requirement | Reason |
|---|---|
| Shared storage | Redis, S3, or similar |
| Serialization | Buffer and Map values |
| Tag indexing | revalidateTag across instances |
| Error handling | Timeouts, dropped connections |
| Size bounding | TTL or eviction policy |
Docker copy order
| Source | Destination |
|---|---|
/app/public | ./public |
/app/.next/standalone | ./ |
/app/.next/static | ./.next/static |
Best Practices
✅ Do This:
// Use standalone output for self-hosting
output: 'standalone' // ✅
// Configure a shared cache handler for multi-instance
cacheHandler: require.resolve('./cache-handler.js') // ✅
# Set a consistent encryption key across instances
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=... // ✅
# Copy static and public separately
COPY --from=builder /app/.next/static ./.next/static // ✅
❌ Don’t Do This:
# Forget to copy the static directory
# Assets return 404 // ❌
// Use the default filesystem cache with multiple instances
// ISR produces inconsistent content // ❌
# Rebuild for each container without a consistent build ID
# The build ID differs across containers // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Static assets return 404 | .next/static not copied | Copy it in the Dockerfile |
| ISR inconsistent across pods | Filesystem cache per instance | Use a shared cache handler |
| Server Action fails across instances | Encryption key differs | Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
| Build is slow or OOMs | Insufficient RAM | Use a host with at least 2 GB for the build |
| Vercel build fails | Wrong framework preset or output directory | Check the project settings |
Real-World Examples
1. Standalone config
output: 'standalone'
2. Dockerfile copy
COPY --from=builder /app/.next/standalone ./
3. Cache handler config
cacheHandler: require.resolve('./cache-handler.js'), cacheMaxMemorySize: 0
4. Encryption key
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=...
5. Build ID
generateBuildId: async () => process.env.GIT_HASH
6. Nginx static caching
location /_next/static { alias /app/.next/static; expires 1y; }
7. Vercel CLI
vercel --prod
8. Vercel override
{ "buildCommand": "npm run build" }
9. Health check
curl http://localhost:3000/api/health
10. Multi-instance warning
Each pod has its own cache unless a shared handler is configured.
Visual
The two deployment paths
┌─────────────────────────────────────────────────────────────┐
│ VERCEL SELF-HOSTING │
│ │
│ Git push ──► Vercel build Git push ──► CI build │
│ │ │ │
│ ▼ ▼ │
│ Auto-detected config output: 'standalone' │
│ │ │ │
│ ▼ ▼ │
│ Vercel infrastructure Docker image / bare metal │
│ │ │ │
│ ▼ ▼ │
│ CDN + functions Reverse proxy + Node │
└─────────────────────────────────────────────────────────────┘
The standalone output
┌─────────────────────────────────────────────────────────────┐
│ .next/standalone/ │
│ ├── server.js │
│ ├── node_modules/ │
│ └── .next/ │
│ │
│ Must copy separately: │
│ ├── public/ │
│ └── .next/static/ │
└─────────────────────────────────────────────────────────────┘
The multi-instance cache problem
┌─────────────────────────────────────────────────────────────┐
│ WITHOUT SHARED CACHE │
│ Instance A revalidates ──► local cache updated │
│ Instance B serves stale ──► no visibility │
│ Load balancer alternates ──► inconsistent content │
│ │
│ WITH SHARED CACHE │
│ Instance A revalidates ──► Redis updated │
│ Instance B revalidates ──► reads Redis │
│ Consistent across instances │
└─────────────────────────────────────────────────────────────┘
The encryption key
┌─────────────────────────────────────────────────────────────┐
│ Build 1: key A ──► Instance 1 encrypts closure │
│ Build 2: key B ──► Instance 2 cannot decrypt │
│ │
│ Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to the same value │
│ on every instance. │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Vercel build | Auto-detected, Git integration |
| Vercel output | Managed, not portable |
| Self-host output | output: 'standalone' |
| Standalone entry | .next/standalone/server.js |
| Static assets | .next/static/ copied separately |
| Multi-instance cache | Shared cache handler |
| Cache handler config | cacheHandler + cacheMaxMemorySize: 0 |
| Multi-instance encryption | NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
| Build ID | generateBuildId |
Key takeaways:
- Vercel is the managed path; self-hosting is the portable path. Vercel detects the framework, configures the build, and handles the CDN and caching. Self-hosting requires the
output: 'standalone'option, a Node.js server, and a reverse proxy . - The standalone output is minimal but incomplete. The
.next/standalonedirectory contains the server and the production dependencies, but thepublic/and.next/static/directories must be copied separately. A Dockerfile that omits them produces a server that returns 404 for every static asset . - Multi-instance self-hosting requires a shared cache handler. The default filesystem cache is per-instance, so ISR and
revalidateTagproduce inconsistent content across pods. The handler stores entries in Redis or S3, serializes theBufferandMapvalues, and indexes them by tag . - Multi-instance self-hosting requires a consistent encryption key. Server Functions encrypt closure variables with a build-specific key. All instances must use the same key, set through
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, or the Server Action fails with “Failed to find Server Action” . - A consistent build ID keeps multiple containers on the same build. The
generateBuildIdoption sets the ID, and using the Git commit hash ensures that every container boots the same version . - The Vercel build is configurable but usually does not need to be. The framework preset handles the build command and output directory. The
vercel.jsonoverrides exist for monorepos and custom pipelines .
Remember: Deployment is a choice between the managed platform and the portable artifact. Vercel provides the CDN, the functions, and the caching without configuration. Self-hosting provides control and cost predictability, but it requires the standalone output, the reverse proxy, the shared cache handler, and the encryption key. The build artifact is the same; the operational surface is not. Get the standalone output and the static asset copy right first, because a server without its static files is a broken deployment. Then add the shared cache handler and the encryption key if more than one instance runs, because a multi-instance deployment without them is an inconsistent one.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!