Deploying Next.js applications
Deploying a Next.js application means building your app and hosting it on infrastructure that can serve its static pages, server-rendered routes, and API endpoints.
Theory
TL;DR
- Vercel splits your app automatically: static pages go to CDN edge, SSR and API routes run as serverless functions
- Docker bundles everything into a single Node.js process - you manage scaling yourself
- Static export generates plain HTML/CSS/JS files with no server required, but kills SSR and API routes entirely
- Decision rule: Vercel for speed and zero ops; Docker for custom infra (Kubernetes, on-prem); static export if you have no server-side logic at all
Quick Example
The fastest path to production is one command with Vercel CLI:
npm install -g vercel@latest
vercel --prod
# Output:
# > Vercel CLI 33.6.2
# > ✅ Production: https://your-app-abc123.vercel.app
# > 📝 Deployed in 45sVercel reads your next.config.js, separates static pages from dynamic ones, and pushes each to the right infrastructure. No config file needed beyond what Next.js already has.
Key Difference
Vercel handles Next.js's hybrid output natively: SSG pages go to CDN edges, SSR and API routes run as serverless Lambdas. Docker gives you one Node.js process running server.js - full control, but you own scaling, HTTPS, and CDN configuration. Static export removes the server entirely, which means near-zero hosting cost but no dynamic rendering.
When to Use
- Prototypes and landing pages: Vercel free tier - instant deploys, HTTPS included, preview URLs per branch
- E-commerce or dashboards with SSR: Vercel or Docker with
output: 'standalone' - Custom infra on Kubernetes or on-prem: Docker standalone - deploy to EC2, Fly.io, or any cluster
- Docs sites and static blogs: Static export to Netlify, S3, or GitHub Pages for near-zero cost
- Existing VPS with Node.js:
npm startorpm2, but move to standalone mode for production
Comparison Table
| Feature | Vercel | Docker (Standalone) | Static Export | Node Server |
|---|---|---|---|---|
| Setup time | One command | Dockerfile + build | next build + upload | pm2/node cluster |
| SSR support | Full (Edge + Serverless) | Full (single process) | No | Full |
| Cost | Free tier, then $20+/mo | EC2 ~$10/mo | ~$0 (S3/Netlify) | Server costs |
| Scaling | Automatic | Manual (Swarm/K8s) | Infinite (CDN) | Manual |
| Custom middleware | Edge Middleware | Any Node.js library | None (no runtime) | Full Node.js |
| When to use | Teams who want zero ops | Monoliths on AWS/GCP | Documentation sites | Quick internal tools |
How the Build Works Internally
next build scans your app/ or pages/ directories and prerenders SSG routes to .next/static/. SSR and API routes get bundled into .next/server/app/. With output: 'standalone', Next.js also copies the minimal Node.js dependencies needed to run server.js without a full node_modules folder present at runtime. That is why multi-stage Docker builds shrink from roughly 1.2GB down to under 150MB - the runner stage only gets the files it actually needs.
Common Mistakes
Not setting output: 'standalone' for Docker
Without it, the Docker image needs the full .next directory and node_modules mounted separately. It works locally but breaks in most container environments where you copy only the build output.
// next.config.js
const nextConfig = {
output: 'standalone', // required for Docker and Fly.io
};
module.exports = nextConfig;Binding to localhost inside Docker
If the container starts fine but requests time out from outside, the server is binding to 127.0.0.1 instead of 0.0.0.0. The standalone server.js respects process.env.HOSTNAME. I have seen teams spend hours on this one - it is a two-line fix:
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "server.js"]Using getServerSideProps with static export
Static export and getServerSideProps are incompatible. The build fails with an error about unsupported pages. Migrate to generateStaticParams in the App Router, or switch to a server deployment.
Forgetting NODE_ENV=production in Docker
Dev mode loads extra code, runs additional checks, and can expose debug information. Always set it in the Dockerfile:
ENV NODE_ENV=productionEdge Runtime with Node.js crypto module
If you move an API route to Edge Runtime for lower latency and it uses Node's crypto module, it will break. Edge runs on V8 isolates without the full Node.js core. Switch to the Web Crypto API:
// Fails on Edge Runtime
import crypto from 'crypto';
const hash = crypto.createHmac('sha256', secret).update(body).digest('hex');
// Works on Edge Runtime
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));Real-World Usage
- nextjs.org runs on Vercel, serving millions of requests per day via Edge
- Vercel Commerce (the official Shopify-like starter) uses Docker standalone for self-hosted deployments on Fly.io and EC2
- Tailwind CSS docs use static export on S3 + CloudFront
- Leonardo.ai runs a Dockerized Next.js frontend on custom Kubernetes infra for GPU workloads
Follow-Up Questions
Q: What is the difference between output: 'standalone' and the default build?
A: The default build outputs .next/ and expects node_modules to be present at runtime. Standalone copies only the files needed to run server.js into .next/standalone/, so you deploy a single directory without reinstalling dependencies.
Q: How does Vercel handle ISR vs SSR at scale?
A: ISR revalidates pages via a webhook to the Edge cache, so most users get a cached response. SSR invokes a Lambda per request, which means cold starts are possible on low-traffic routes.
Q: Why use Edge Runtime instead of Node.js runtime on Vercel?
A: Edge runs on V8 isolates distributed globally, giving roughly 10ms latency worldwide. Node.js runtime runs in specific AWS regions and has higher cold starts. The tradeoff is that Edge has no access to the full Node.js API, including built-in modules like fs or crypto.
Q: What are the real size numbers for Docker multi-stage builds?
A: A single-stage build with full node_modules is around 1.2GB. With output: 'standalone' and a multi-stage Dockerfile, the final runner image is typically 140-160MB. That is roughly 85% smaller.
Q: Your Vercel deployment hits the 15-second timeout on cold start because of a heavy ML library. What do you do?
A: Split the heavy processing into a separate serverless function or background job. Use reserved concurrency to keep a warm Lambda instance. Or move ML inference to an external service like Replicate or Modal and call it via HTTP from a lightweight Next.js API route - in production this brought one team's cold start from 7s down to 1.2s.
Examples
Basic: Deploy to Vercel with One Command
# In your Next.js project root
npm install -g vercel@latest
vercel --prod
# Vercel detects Next.js, builds it, splits static/dynamic output,
# and returns a production URL with HTTPS and global CDNNo extra configuration required. Vercel reads your existing next.config.js and handles routing, caching, and HTTPS automatically.
Intermediate: Docker Standalone for Self-Hosted SSR
// next.config.js
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
COPY /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]docker build -t my-nextjs-app .
docker run -p 3000:3000 my-nextjs-appThe multi-stage build drops image size from ~1.2GB to around 150MB. SSR pages, API routes, and static assets all work. Deploy this image to EC2, Fly.io, or any Kubernetes cluster.
Advanced: Edge Runtime Gotcha with Webhook Signature Validation
A common mistake is moving a Stripe webhook handler to Edge Runtime without updating the crypto code.
// app/api/webhook/route.ts
// ❌ Breaks on Edge Runtime - Node crypto is not available
import crypto from 'crypto';
export const runtime = 'edge';
export async function POST(req: Request) {
const sig = req.headers.get('x-signature') ?? '';
const body = await req.text();
// Throws: "The "crypto" module is not available in Edge Runtime"
const hash = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(body)
.digest('hex');
}// ✅ Fix: Web Crypto API works on Edge Runtime (V8 isolates)
export const runtime = 'edge';
export async function POST(req: Request) {
const sig = req.headers.get('x-signature') ?? '';
const body = await req.text();
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(process.env.WEBHOOK_SECRET!),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
const hashHex = Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
if (sig !== `sha256=${hashHex}`) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
return Response.json({ ok: true });
}crypto.subtle (Web Crypto API) is available in both Edge Runtime and Node.js runtime, so this code also works if you later switch back to the Node.js runtime without any changes.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.