Suggest an editImprove this articleRefine the answer for “Deploying Next.js applications”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Deploying a Next.js application** comes down to three options: Vercel (managed, zero config), Docker with `output: 'standalone'` (self-hosted, full SSR), or static export (no server, any CDN). ```bash vercel --prod # ✅ Production: https://your-app.vercel.app (45s) ``` **Key point:** for any deployment outside Vercel, add `output: 'standalone'` to `next.config.js` first.Shown above the full answer for quick recall.Answer (EN)Image**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: ```bash npm install -g vercel@latest vercel --prod # Output: # > Vercel CLI 33.6.2 # > ✅ Production: https://your-app-abc123.vercel.app # > 📝 Deployed in 45s ``` Vercel 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 start` or `pm2`, 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. ```js // 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: ```dockerfile 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: ```dockerfile ENV NODE_ENV=production ``` **Edge 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: ```js // 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 ```bash # 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 CDN ``` No extra configuration required. Vercel reads your existing `next.config.js` and handles routing, caching, and HTTPS automatically. ### Intermediate: Docker Standalone for Self-Hosted SSR ```js // next.config.js const nextConfig = { output: 'standalone', }; module.exports = nextConfig; ``` ```dockerfile 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 --from=deps /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 --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/public ./public USER nextjs EXPOSE 3000 CMD ["node", "server.js"] ``` ```bash docker build -t my-nextjs-app . docker run -p 3000:3000 my-nextjs-app ``` The 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. ```ts // 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'); } ``` ```ts // ✅ 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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.