Production Builds

Preparing and Inspecting a Production Build

Every deployment target starts the same way: next build compiles the app into .next, and next start serves what it produced. A host that knows nothing about Next.js 10,514 needs nothing more than "build" and "start" scripts in package.json. The build prints a route table, and reading it is the cheapest deployment check there is. Here is what npx next build reports for the demo app, whose /prices page sets revalidate = 15:

Output of 98
▲ Next.js 16.3.5 (Turbopack)
✓ Compiled successfully in 2.7s
✓ Generating static pages using 7 workers (5/5) in 709ms
Route (app)      Revalidate  Expire
┌ ○ /
├ ○ /_not-found
├ ƒ /api/health
├ ○ /prices             15s      1y
└ ƒ /status

○ marks a route rendered once, during the build; ƒ one that runs your code on every request. Revalidate is the revalidation window, Expire the limit past which a stale entry may no longer be served. A route you expected to be ○ showing up as ƒ is the most common deployment surprise: something in it read cookies(), headers() or searchParams, so the whole route opted into dynamic rendering (Rendering Modes). Fix that before you scale the server, not after.

Two files in .next matter to a deployment. BUILD_ID identifies the build and is baked into every asset URL, so a browser holding a page from build 1 cannot load a chunk from build 2. The *.nft.json trace files list which files the server needs, which is what makes the standalone output of The standalone Output possible.

One build, many environments

The rule from Environment Variables — NEXT_PUBLIC_* is inlined at build time, everything else stays on the server — becomes operational once you deploy. An inlined value is part of the artifact and cannot change without a rebuild; a server-side variable can, provided the code reading it runs at request time. Reading process.env in a prerendered component freezes the build machine's value into the HTML, and connection() prevents that by marking the component request-time.

app/status/page.js — an environment variable read on every requestJavaScript
import { connection } from 'next/server'
export default async function Status() {
  await connection()
  return <pre>{process.env.API_BASE_URL ?? '(unset)'}</pre>
}

That is what lets one image be promoted from staging to production with nothing but a different --env-file.

Builds that must be reproducible

Two next.config.js settings matter once more than one instance serves the same build. generateBuildId: async () => process.env.GIT_HASH replaces the random build ID with something you control, so ten containers built from one commit agree on asset URLs. deploymentId: process.env.DEPLOYMENT_VERSION goes further: assets get a ?dpl= query parameter and client navigations send an x-deployment-id header, so when a browser from the old deployment reaches a server from the new one, Next.js forces a full page load instead of a broken client navigation.

For bundle analysis, see Analyzing the Bundle.