5. Deployment
How a Next.js app actually ships. See also Vercel, Render and CI/CD.
1. Build Output
npm run build # next build — Turbopack by default in Next 16
npm run start # next start — production serverReading 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 demandFirst 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 239ms2. Deployment Targets
| Target | How | Good for |
|---|---|---|
| Vercel | git push | Zero config, every feature supported |
| Docker / self-host | next start behind a reverse proxy | Full control, compliance requirements |
| Render / Railway / Fly | Node web service | Cheap always-on server, fixed pricing |
| Static export | output: "export" | Pure static host (S3, GitHub Pages) |
| Cloudflare Workers | OpenNext adapter | Edge-first, low cost |
3. Static Export
// 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)
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.
# 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, notnpm 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
| Variable | Available in | Notes |
|---|---|---|
DATABASE_URL | Server only | Never reaches the browser |
NEXT_PUBLIC_API_URL | Server + browser | Inlined 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, committedserverRuntimeConfig and publicRuntimeConfig were removed in Next.js 16 — .env files only.
6. Caching Headers and CDN
Next.js sets sensible defaults; override when needed:
// 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
| Concern | What you must handle yourself |
|---|---|
| ISR cache | Configure a shared cache handler, or each instance caches separately |
| Image optimisation | Runs on your server — CPU and memory cost |
| Multiple instances | Sticky sessions not needed, but the cache must be shared |
| Static assets | Put a CDN in front of /_next/static |
| Zero-downtime | Health checks + rolling restarts |
| Logs and metrics | Wire up your own |
// 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 buildoutput reviewed — no route with an unexpected First Load JS - [ ]
@next/bundle-analyzerrun — no surprise 400 kB dependency - [ ] All images use
next/imagewith correctsizes - [ ] 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
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
| Metric | Good | Usual Next.js fix |
|---|---|---|
| LCP (load) | < 2.5s | priority on the hero image, server render the shell |
| INP (interactivity) | < 200ms | Less client JavaScript, break up long tasks |
| CLS (stability) | < 0.1 | next/image dimensions, next/font |
9. Monitoring
// 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.
// 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.remotePatternsrestricted to hosts you trust — a wildcard turns your image endpoint into an open proxy - [ ] Rate limiting on auth and mutation endpoints
- [ ]
dangerouslySetInnerHTMLonly 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.