Deployment & Runtime Choices
Choose the Node vs Edge runtime deliberately, ship a lean standalone build for Docker, and get caching right behind a self-hosted server.
Deploying Next.js well means making a few explicit choices instead of accepting defaults blindly.
Node vs Edge runtime
| Node.js (default) | Edge | |
|---|---|---|
| APIs | full Node — DB drivers, fs, crypto | Web-standard subset only |
| Cold start | slower | near-zero |
| Location | regional | globally distributed |
| Best for | data-heavy routes, ORMs | light redirects, personalization, geo |
Set it per route with export const runtime = 'edge'. Do not force Edge on a route that uses a native database driver — it will not run there. Middleware is Edge-only.
Self-hosting: the standalone output
Set output: 'standalone' in next.config and the build emits a minimal folder with only the files and node_modules the server actually needs. That is the basis of a small Docker image — copy the standalone output, the static assets, and the public folder, then run node server.js. No need to ship the whole repo.
Caching when self-hosted
On Vercel the caching layers 'just work'. Self-hosted, the Data Cache and ISR write to the filesystem by default, which does not survive across multiple container replicas. For horizontal scaling, configure a shared cache handler (e.g. Redis) via cacheHandler so all instances see the same revalidations.
Example
// next.config.ts — lean output for containers
import type { NextConfig } from 'next';
const config: NextConfig = {
output: 'standalone',
experimental: {
// Optional: shared cache across replicas when self-hosting
// cacheHandler: require.resolve('./cache-handler.js'),
optimizePackageImports: ['lucide-react', 'date-fns'],
},
};
export default config;
// Per-route runtime selection:
// app/geo/route.ts
export const runtime = 'edge'; // fast, global, Web APIs only
// Dockerfile (sketch):
// COPY --from=builder /app/.next/standalone ./
// COPY --from=builder /app/.next/static ./.next/static
// COPY --from=builder /app/public ./public
// CMD ["node", "server.js"]When to use it
- A team ships a Next.js app in a Docker container using standalone output to reduce the image from 2 GB to under 300 MB.
- A developer selects the Edge runtime for a middleware that runs geo-routing so it starts in under 1 ms on every request.
- An ops engineer configures nginx with stale-while-revalidate headers to serve cached Next.js pages during ISR regeneration.
More examples
Edge runtime for a route handler
Exporting runtime = 'edge' runs this handler in the V8 isolate on Vercel's edge network, with sub-millisecond cold starts.
// app/api/geo/route.ts
export const runtime = 'edge';
import { NextRequest, NextResponse } from 'next/server';
export function GET(req: NextRequest) {
const country = req.geo?.country ?? 'unknown';
return NextResponse.json({ country });
}Standalone Docker image
Multi-stage build produces a minimal image: only the standalone Node server, static files, and public assets.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Nginx cache headers for ISR
Nginx caches Next.js SSR responses for 60 seconds in front of the Node server, reducing load during ISR regeneration.
# /etc/nginx/conf.d/nextjs.conf
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=nextjs:10m;
server {
location / {
proxy_pass http://localhost:3000;
proxy_cache nextjs;
proxy_cache_valid 200 60s;
add_header X-Cache-Status $upstream_cache_status;
}
}
Discussion