Building and deploying

What next build produces, how to read the route summary, and the deployment targets for a Node server, a container or static hosting.

What the build tells you

npx create-next-app@latest my-app --ts --app --eslint

npm run dev        # development server with fast refresh
npm run build      # production build; reports route types and sizes
npm start          # serve the production build on a Node server

# a self-contained server bundle for containers
# next.config.js -> output: 'standalone'

The build output marks each route as static, dynamic or a server function, and shows the first-load JavaScript for each. Read that table after every change: an accidentally dynamic page and a growing bundle are both visible there before users notice them.

Route typeRenderedTypical cause
StaticAt build time, cachedNo request-specific data
DynamicPer requestCookies, headers, search params, no-store
ISR / revalidatedCached then refreshedrevalidate on a fetch or route segment
Route handlerOn demandroute.js endpoint
  • generateStaticParams prerenders a known set of dynamic routes; anything not listed is rendered on demand.
  • next/image optimises and lazy-loads images, and needs allowed remote hosts in the config.
  • next/font self-hosts fonts at build time, which removes a render-blocking request and a layout shift.

Configuration that matters

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',              // minimal server bundle for Docker
  images: {
    remotePatterns: [{ protocol: 'https', hostname: 'images.example.com' }]
  },
  env: { SITE_NAME: 'Guides' },      // inlined at build time - not for secrets
  async redirects() {
    return [{ source: '/old-guide', destination: '/guides/intro', permanent: true }]
  }
}

module.exports = nextConfig
  • Environment variables are read at build time for anything inlined and at runtime for server code — a container built with one set of values will not pick up new ones without a rebuild if the value was inlined.
  • Prefix with NEXT_PUBLIC_ only what the browser may see; keep the rest server-side.
  • redirects and rewrites in the config are evaluated before routing to your pages, which is the cheapest place to handle a URL migration.
  • output: 'standalone' traces the files the server actually imports, so a container image does not need your whole node_modules.

Where to run it

TargetBuild outputNotes
Managed platform (Vercel and similar)next buildZero config; static assets on a CDN, functions per route
Node server on a VPSnext build + next startYou manage processes, TLS and a reverse proxy
Container (Docker, Kubernetes)output: 'standalone'Copy .next/standalone plus .next/static and run server.js
Static exportoutput: 'export'No server features: no route handlers, no server actions, no on-demand revalidation
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
⚠️
Static export removes anything that needs a server: route handlers, server actions, and on-demand revalidation all stop working. Decide that trade-off before you build the site around them, not when the deploy fails.

FAQ

Why does my build fail with a window or document reference?
A client-only API ran during server rendering. Add 'use client' to that component and move the access into an effect, or guard the render with a mounted flag.
Should I use static export for a content site?
If every page can be prerendered at build time and you have no per-request behaviour, yes — it is cheaper and simpler. The moment you need middleware, cookies or server actions, choose a server target instead.

Data fetching and server components Effects and data fetching

Last refreshed 2026-09-18.