Self-Hosting Behind nginx

"To run Next.js 10,514 , your platform needs a Node.js 2,131 server. That's it." The documentation's own summary settles an argument that keeps being had: one next start process handles Server Components, ISR, Partial Prerendering, Cache Components, Server Actions, the proxy and after() correctly. Extra infrastructure buys performance and multi-instance consistency, not correctness.

Running Behind a Reverse Proxy and Running Behind nginx cover reverse proxies in general; three things are specific to a Next.js upstream. Hashed assets under /_next/static/ are immutable and should never reach Node. Streaming responses must not be buffered, or loading.js, Suspense and PPR silently become a slow, all-at-once render. And Server Actions post real payloads, so the default 1 MB body limit is too small for a form with a file in it.

/etc/nginx/sites-available/example.comCSS
server {
  listen 443 ssl http2;
  server_name example.com;
  client_max_body_size 10m;
  location /_next/static/ {
    alias /var/www/app/.next/static/;
    add_header Cache-Control "public, max-age=31536000, immutable";
  }
  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;          # let streamed responses through
  }
}

proxy_buffering off is the local switch. When the proxy is not yours to configure, ask from the application side instead: a headers() entry in next.config.js setting X-Accel-Buffering: no on /:path*{/}? is honored by nginx 75 and several other proxies. Check the whole path, not just the last hop; a load balancer that buffers (AWS 24 ALB in front of Lambda is the documented example) cancels streaming even when the origin is right.

Health checks and shutdown

No health endpoint is built in, and / is a poor substitute: it may be a cached static page that answers happily while the process is sick. A route handler marked dynamic proves the event loop is running:

app/api/health/route.jsJavaScript
export const dynamic = 'force-dynamic'
export async function GET() {
  return Response.json({ status: 'ok', uptime: Math.round(process.uptime()) },
    { headers: { 'Cache-Control': 'no-store' } })
}
Output
$ curl -s http://127.0.0.1:3115/api/health
{"status":"ok","uptime":11}

A readiness probe may ping MongoDB 1,815 or the Express 24,430 API too, but keep liveness cheap: a probe that fails because a downstream service is slow restarts a perfectly healthy container.

Run the process under a supervisor — systemd 142,543 , pm2 7.0.4 29,762 , or your orchestrator's restart policy (Process Managers). On shutdown, send SIGINT or SIGTERM and wait: the server finishes in-flight requests and runs pending after() callbacks before exiting. Allow 10 to 30 seconds to drain, the window a blunt kill -9 takes away.