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
APIsfull Node — DB drivers, fs, cryptoWeb-standard subset only
Cold startslowernear-zero
Locationregionalglobally distributed
Best fordata-heavy routes, ORMslight 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

Example · typescript
// 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.

Example · ts
// 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.

Example · dockerfile
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.

Example · bash
# /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

  • Be the first to comment on this lesson.
Deployment & Runtime Choices — Next.js | SoundsCode