Next.js10 min read
Deploy Next.js: Vercel vs Self-Hosting with Docker
Deploy Next.js on Vercel or self-host with Docker and standalone output. Compare costs, features, caching, ISR and the gotchas before you choose a host.
You can deploy Next.js in two main ways: push it to Vercel and let the platform handle builds, CDN, caching and scaling, or self-host it as a Node.js server, usually inside a Docker image built with output: 'standalone'. Both support every Next.js feature. The real difference is who runs the infrastructure, how you pay for it, and how much work caching takes once you have more than one server.
I have shipped Next.js apps both ways. This guide covers what each option gives you, a production Dockerfile that actually works, and the problems that only show up after the first deploy.
Key takeaways
- The official Next.js docs list four deployment targets: Node.js server, Docker container, static export and platform adapters. Node and Docker support all features.
output: 'standalone'produces a smallserver.jsplus only the traced dependencies, which makes Docker images much leaner.- Self-hosting a single instance is simple. Running several instances means you must share the cache, the Server Actions encryption key and a deployment ID.
- Vercel is the least work. Self-hosting gives you a predictable bill and full control, and costs you ops time.
- Put a reverse proxy like nginx in front of a self-hosted server, and turn off response buffering so streaming works.
Next.js deployment options at a glance
As of Next.js 16 (the current line is 16.4), the deployment page in the docs groups everything into four options. Here is how they compare in practice.
| Option | Feature support | Who runs it | Good fit |
|---|---|---|---|
| Vercel (verified adapter) | All | Vercel | Teams that want zero ops, preview deploys, global CDN |
Node.js server (next start) |
All | You | A VPS, a PaaS like Railway or Render |
Docker container (standalone) |
All | You | Kubernetes, Cloud Run, Fly.io, any container host |
Static export (output: 'export') |
Limited | Any static host | Marketing sites, docs, blogs with no server logic |
Next.js 16.2 also made the Deployment Adapter API stable, so other platforms can build first-class integrations. Vercel and Bun are listed as verified adapters, and Cloudflare and Netlify ship their own integrations while they work on verified ones. If you plan to deploy somewhere other than Vercel or your own Node server, check that platform's feature list before you rely on ISR, streaming or the image optimizer.
Deploying Next.js on Vercel
Vercel builds the framework, so the path is short: connect the Git repository, and every push to main becomes a production deploy while every pull request gets a preview URL. You do not write a Dockerfile, and you do not configure a CDN or cache.
What you get without extra work:
- Preview deployments per branch, which are great for design and QA review.
- A global CDN for static assets and prerendered pages.
- ISR,
revalidatePath,revalidateTagand Cache Components working across the whole fleet, because the cache is shared by the platform. - Image optimization, logs, rollbacks and environment variables per environment.
What to watch:
- Pricing is usage-based. On paid plans you pay for things like function execution, bandwidth and image optimization beyond the included amounts. A traffic spike or a badly cached page can show up on the invoice. Set spend limits and check the usage dashboard after launches.
- The Hobby plan is for personal, non-commercial projects. Client work and company products belong on a paid plan.
- Lock-in is mostly operational, not code-level. Your Next.js code runs elsewhere, but features you lean on (preview comments, edge config, platform analytics) will need replacements.
For a small team shipping a product, Vercel is usually the right default. You only start questioning it when the bill grows faster than the traffic, or when the company needs everything inside its own cloud account.
Self-hosting Next.js with Docker and standalone output
Self-hosting starts with one line in your config.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
}
export default nextConfigWhen you run next build, Next.js traces every file each route needs and copies them into .next/standalone, along with a minimal server.js. You no longer need the full node_modules folder in production.
Two folders are not copied for you: public and .next/static. The docs leave them out on purpose, because ideally a CDN serves them. If you want server.js to serve them, copy them in:
cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
node .next/standalone/server.jsForgetting this step is the most common reason a self-hosted build loads with no CSS and broken JavaScript.
A production Dockerfile for Next.js
This is a multi-stage build based on the pattern in the official with-docker example. It installs dependencies once, builds, then copies only the standalone output into a small runtime image that runs as a non-root user.
# syntax=docker/dockerfile:1
FROM node:24-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:24-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]A few notes:
HOSTNAME=0.0.0.0matters. Without it the server may listen only inside the container and your port mapping will look dead.- The
--chown=node:nodeflags let the server write to.nextat runtime, which ISR and image optimization need for their disk cache. - If your project has no
publicfolder, delete thatCOPYline or the build fails. - Next.js 16 needs Node.js 20.9 or newer. Pin a current LTS image and update it regularly.
Add a .dockerignore so you do not ship your local build and secrets into the build context:
node_modules
.next
.git
.env*.localBuild and run:
docker build -t my-next-app .
docker run -p 3000:3000 --env-file .env.production my-next-appEnvironment variables: build time vs runtime
This catches almost everyone once. Variables prefixed with NEXT_PUBLIC_ are inlined into the JavaScript bundle during next build. Changing them on the running container does nothing; you have to rebuild.
Server-only variables (no prefix) can be read at runtime, but only in code that renders dynamically. If a page is prerendered at build time, the value from the build is baked into the HTML. The docs show calling await connection() from next/server before reading process.env to force request-time evaluation. That is what lets you build one image and promote it from staging to production with different values.
Caching, ISR and multiple instances when self-hosting
A single next start instance with a persistent disk just works. ISR, revalidatePath and revalidateTag use a cache that lives in memory and on local disk.
Things change when you scale out to two or more containers. Each one has its own cache, so a revalidation on pod A does not reach pod B. Users get different versions of the same page depending on which pod answers.
The docs list what a multi-instance setup needs:
| Problem | Fix |
|---|---|
| Each instance has its own ISR cache | Set cacheHandler to a shared store (Redis, S3) and cacheMaxMemorySize: 0 |
revalidateTag only clears one instance |
Sync tags through shared storage; for 'use cache', implement refreshTags() in a cacheHandlers handler |
| "Failed to find Server Action" errors | Set the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build time |
| Old clients requesting deleted assets during rolling deploys | Set deploymentId so Next.js detects version skew and hard-reloads |
| Different build IDs across stages | Use generateBuildId, for example returning the git hash |
A minimal config for a multi-instance deploy looks like this:
// next.config.js
module.exports = {
output: 'standalone',
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0,
deploymentId: process.env.DEPLOYMENT_VERSION,
}The cache handler is a class with get, set, revalidateTag and resetRequestCache. Do not write it from scratch on day one. Start from the official Redis example in the Next.js repo, and read the warning in the docs about serializing Buffer and Map values, because plain JSON.stringify breaks client navigation in subtle ways.
This is where self-hosting gets real. On Mercury 360, an AI HR platform I worked on, pages are pre-rendered and revalidated in the background, and that whole model depends on the cache behaving the same on every server. If you move an ISR-heavy app off Vercel, budget time for this part. If you want a refresher on how the cache layers fit together, read my guide to Next.js caching.
Reverse proxy, streaming and graceful shutdown
The docs recommend not exposing the Next.js server directly to the internet. Put nginx, Caddy or your cloud load balancer in front to handle TLS, slow clients, payload limits and rate limiting.
One gotcha: nginx buffers responses by default, which breaks streaming and Suspense. The page arrives all at once instead of in chunks. You can tell nginx to stop buffering by sending a header from Next.js:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*{/}?',
headers: [{ key: 'X-Accel-Buffering', value: 'no' }],
},
]
},
}Also make sure your orchestrator sends SIGTERM and waits before killing a container. Next.js finishes in-flight requests and pending after() callbacks on shutdown, and the docs suggest a 10 to 30 second drain period.
Static export: the cheapest Next.js deployment
If your site has no per-request logic, output: 'export' writes plain HTML, CSS and JS to an out folder that any static host (S3, nginx, GitHub Pages) can serve.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export',
}
export default nextConfigYou lose everything that needs a server: Proxy (the Next.js 16 name for middleware), Server Actions, ISR, cookies and headers at request time, and the default image optimizer (you need a custom loader or unoptimized). For a blog or a portfolio, that trade is often worth it. My notes on Next.js SEO with metadata and sitemaps work fine with static export.
Vercel vs self-hosting: how I decide
Here is the checklist I use with my team:
- Choose Vercel when the team is small, nobody wants to own servers, you value preview deploys, and traffic is moderate or unpredictable.
- Choose a single Docker container or VPS when traffic is steady, you want a fixed monthly cost, and one or two instances are enough.
- Choose a container platform with a shared cache when compliance requires your own cloud, or you need many instances. Plan for Redis, a CDN for
/_next/static, and proper monitoring. - Choose static export when nothing on the site needs a server.
Cost comparisons online often skip the hidden line item: your time. A VPS can cost less than a Vercel bill, but someone has to patch Node, renew certificates, apply the monthly Next.js security releases and wake up when the disk fills. If that someone is you, count those hours.
Common mistakes when deploying Next.js
- Not copying
publicand.next/staticinto the standalone folder. The site renders without styles. - Expecting
NEXT_PUBLIC_variables to change at runtime. They are frozen at build. - Scaling to several pods without a shared cache. Pages flip between old and new content.
- Running behind nginx with buffering on. Streaming and
loading.tsxstop feeling fast. - Skipping security updates. The Next.js team now publishes scheduled security releases. Self-hosters have to apply them; nobody does it for you.
- Building inside the production container on startup. Build once in CI, then run the same image everywhere.
If you are still choosing between frameworks, my comparison of Vite, Next.js and React Router covers the hosting implications of each. The official self-hosting guide is worth reading end to end before you go to production.
FAQ
Can I deploy Next.js without Vercel?
Yes. The Next.js docs list a Node.js server (next build and next start) and a Docker container as deployment options that support every feature. You can also use a static export for sites that do not need a server, or a platform adapter.
What does output: 'standalone' do in Next.js?
It makes next build create a .next/standalone folder with a minimal server.js and only the node_modules files your app actually needs. You copy public and .next/static next to it yourself, then run node server.js.
Does ISR work when self-hosting Next.js?
Yes. ISR and revalidation work with next start, using a cache stored on the local disk of each instance. If you run several instances, configure a shared cache handler (for example Redis) so revalidation reaches all of them.
Is self-hosting Next.js cheaper than Vercel?
Often, for steady traffic on a single server, but not automatically. You trade a usage-based bill for fixed server costs plus your own time on patching, scaling, caching, monitoring and security updates.
Wrapping up
Vercel is the shortest path from commit to production, and self-hosting with output: 'standalone' is a solid, fully supported alternative. The Dockerfile is the easy part. The hard part is caching, keys and deployment IDs once you run more than one instance, so decide on that before you migrate, not after.
- #nextjs
- #deployment
- #docker
- #vercel
- #devops
Keep reading
Next.js8 min read
Next.js App Router vs Pages Router: Migration Guide
Next.js App Router vs Pages Router explained: the real differences, when to migrate, and a step-by-step migration guide updated for Next.js 16.4.
Read articleNext.js8 min read
Next.js Authentication Guide: Sessions, proxy.ts, DAL
Next.js authentication guide for Next.js 16: pick an auth library, manage sessions, protect routes with proxy.ts and secure Server Components and Actions.
Read articleNext.js8 min read
Next.js Caching Explained: use cache, cacheLife, ISR
Next.js caching explained for Next.js 16: 'use cache', cacheLife, cacheTag, revalidateTag vs updateTag vs revalidatePath, ISR and fetch caching.
Read article