Skip to content

5. Deployment

How a Next.js app actually ships. See also Vercel, Render and CI/CD.


1. Build Output

bash
npm run build   # next build — Turbopack by default in Next 16
npm run start   # next start — production server

Reading the build output is a real interview skill:

Route (app)                    Size     First Load JS
┌ ○ /                          1.2 kB   89 kB
├ ● /blog/[slug]               2.1 kB   91 kB
├ ƒ /dashboard                 4.5 kB   94 kB
└ ○ /about                     0.8 kB   88 kB

○  (Static)   prerendered as static content
●  (SSG)      prerendered with generateStaticParams
ƒ  (Dynamic)  server-rendered on demand

First Load JS is the number that matters. Above ~150 kB, first paint suffers on mobile networks.

Next 16 Build Output

The build now reports where time is spent per step:

▲ Next.js 16 (Turbopack)
✓ Compiled successfully in 615ms
✓ Finished TypeScript in 1114ms
✓ Collecting page data in 208ms
✓ Generating static pages in 239ms

2. Deployment Targets

TargetHowGood for
Vercelgit pushZero config, every feature supported
Docker / self-hostnext start behind a reverse proxyFull control, compliance requirements
Render / Railway / FlyNode web serviceCheap always-on server, fixed pricing
Static exportoutput: "export"Pure static host (S3, GitHub Pages)
Cloudflare WorkersOpenNext adapterEdge-first, low cost

3. Static Export

ts
// next.config.ts
const nextConfig = {
  output: "export",
  images: { unoptimized: true },
};

Produces a plain out/ folder of HTML, CSS and JS.

What You Lose

Server Components with runtime data, Route Handlers, Server Actions, middleware/proxy, ISR, next/image optimisation, cookies and headers. Everything that needs a server.

Only use this for a genuinely static site.


4. Standalone Output (Docker)

ts
const nextConfig = { output: "standalone" };

Next.js traces exactly which node_modules files are used and copies only those. Image size drops from ~1 GB to ~150 MB.

dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Points Interviewers Look For

  • Multi-stage build — build tools never reach the final image
  • npm ci, not npm install — reproducible from the lockfile
  • Non-root user — a container escape shouldn't land on root
  • Alpine base — smaller attack surface
  • Note: Next.js 16 requires Node 20.9+

5. Environment Variables

VariableAvailable inNotes
DATABASE_URLServer onlyNever reaches the browser
NEXT_PUBLIC_API_URLServer + browserInlined into the bundle at build time

Build-Time Inlining Trap

NEXT_PUBLIC_* values are baked in during next build. Changing one in your host's dashboard does nothing until you rebuild. This trips people up on every platform and is a good interview answer.

File Precedence

.env.local        # local secrets, gitignored, wins over the rest
.env.production   # production defaults
.env.development  # dev defaults
.env              # shared defaults, committed

serverRuntimeConfig and publicRuntimeConfig were removed in Next.js 16.env files only.


6. Caching Headers and CDN

Next.js sets sensible defaults; override when needed:

ts
// next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "X-Frame-Options", value: "DENY" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
          {
            key: "Strict-Transport-Security",
            value: "max-age=63072000; includeSubDomains; preload",
          },
        ],
      },
    ];
  },
};

Cache Scoping

All Next.js caches are scoped to a single deployment — the cache key includes the build ID. A new deploy starts with a cold cache. That is by design: it prevents a new build from serving markup generated by the old one.


7. Self-Hosting Checklist

ConcernWhat you must handle yourself
ISR cacheConfigure a shared cache handler, or each instance caches separately
Image optimisationRuns on your server — CPU and memory cost
Multiple instancesSticky sessions not needed, but the cache must be shared
Static assetsPut a CDN in front of /_next/static
Zero-downtimeHealth checks + rolling restarts
Logs and metricsWire up your own
ts
// Shared cache across instances when self-hosting
const nextConfig = {
  cacheHandler: require.resolve("./cache-handler.js"),
  cacheMaxMemorySize: 0,   // disable the per-instance in-memory cache
};

Interview Point

"Vercel is expensive, we'll self-host" is a valid decision — but the honest follow-up is that you take on ISR cache coordination, image optimisation cost, and CDN configuration. Say the trade-off; don't pretend it's free.


8. Performance Checklist

Before Deploying

  • [ ] next build output reviewed — no route with an unexpected First Load JS
  • [ ] @next/bundle-analyzer run — no surprise 400 kB dependency
  • [ ] All images use next/image with correct sizes
  • [ ] Above-the-fold hero image has priority
  • [ ] Fonts loaded via next/font
  • [ ] "use client" pushed as far down the tree as possible
  • [ ] Heavy client components dynamically imported
  • [ ] Core Web Vitals checked with Lighthouse

Dynamic Import for Heavy Client Code

tsx
import dynamic from "next/dynamic";

const Chart = dynamic(() => import("@/components/Chart"), {
  loading: () => <Skeleton />,
  ssr: false,   // skip server rendering for browser-only libraries
});

Core Web Vitals

MetricGoodUsual Next.js fix
LCP (load)< 2.5spriority on the hero image, server render the shell
INP (interactivity)< 200msLess client JavaScript, break up long tasks
CLS (stability)< 0.1next/image dimensions, next/font

9. Monitoring

tsx
// app/layout.tsx
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

<body>
  {children}
  <Analytics />
  <SpeedInsights />
</body>

Platform-independent options: Sentry for errors, PostHog for product analytics, OpenTelemetry for traces.

ts
// instrumentation.ts — runs once when the server starts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }
}

10. Security Checklist

  • [ ] No secret behind a NEXT_PUBLIC_ prefix
  • [ ] Every Server Action authenticates inside the action — it is a public endpoint
  • [ ] Every Route Handler validates input (Zod) and checks authorisation
  • [ ] Auth cookies are httpOnly, secure, sameSite
  • [ ] Security headers set (see section 6)
  • [ ] images.remotePatterns restricted to hosts you trust — a wildcard turns your image endpoint into an open proxy
  • [ ] Rate limiting on auth and mutation endpoints
  • [ ] dangerouslySetInnerHTML only with sanitised HTML
  • [ ] Dependencies audited (npm audit, Dependabot)
  • [ ] Error responses generic — log the detail server-side, never return stack traces

Most Common Real Mistake

A secret leaked through NEXT_PUBLIC_, or a Server Action that trusts the client because "the button is only shown to admins". Both are worth naming unprompted in an interview — it signals you have shipped something real.

© 2025 DDocs · Dipak's Documentation Guide