Deployment guides/Next.js/2026

How to deploy a Next.js app

Next.js needs one thing to run everything it can do: a Node.js server. The interesting part of a deploy is not which features survive, it is what changes the moment a second instance starts serving traffic. This guide covers the build, the three output shapes, the proxy in front, and the four settings that scaled deploys get wrong.

Quick answer

To deploy a Next.js app:

  • Build and run → next build then next start. One Node process supports every feature: Server Components, ISR, PPR, Server Actions, Proxy and after().
  • Ship it → the built repo on any Node host, an output: 'standalone' bundle, or a Docker image. output: 'export' gives plain static files but drops ISR, Proxy, Server Actions and default image optimization.
  • Scale it → a second instance needs a shared cacheHandler, one NEXT_SERVER_ACTIONS_ENCRYPTION_KEY and a deploymentId, or you get stale pages and failed Server Actions.
  • Automate it → run the build in CI/CD so those build-time values, and the standalone copy step, are identical on every deploy.

Next.js 16.3.4 · App Router · self-hosted, Docker and static export covered · last updated September 2026

Before you start

What a Next.js deploy actually is

Next.js builds into a server, not a folder. next build compiles the app and prerenders whatever it can; next start then runs a Node process that serves the prerendered output, renders the dynamic parts on demand, handles Server Actions, revalidates ISR pages and optimizes images. That process is the whole runtime.

This matters because the common assumption is backwards. Next.js' own platform guide puts it plainly: "To run Next.js, your platform needs a Node.js server. That's it." and "A single next start process handles every Next.js feature correctly: Server Components, ISR, PPR, Cache Components, Server Actions, Proxy, and after()." The only extra dependency is sharp, needed for image optimization. Everything else that a managed platform adds, CDN caching, edge compute, a shared cache, changes performance and multi-instance consistency, not whether a feature works.

The docs describe four deploy shapes with a stated support level: a Node.js server (all features), a Docker container (all features), a static export (limited), and adapters (varies). Two of those are one decision: a container is just a Node.js server with its filesystem pinned. So in practice you choose between running a server and giving up the server, and then you deal with what a second copy of that server implies.

One rename to know before you read older tutorials: since 16.0.0, Middleware is deprecated and renamed to Proxy, and it now defaults to the Node.js runtime. The runtime config option is gone and throws if you set it. The codemod npx @next/codemod@canary middleware-to-proxy . does the rename for you.

Prerequisites

What you need first

Short list. Most of it you already have.

Somewhere to run Node

A VPS, a container platform, a managed Node host: anything that keeps a process alive and gives it a port. Your package.json needs build and start scripts, which create-next-app writes for you.

sharp, if you use next/image

The one additional dependency for image optimization. On glibc-based Linux it may need extra configuration to keep memory use in check.

A reverse proxy

The docs recommend nginx or similar in front of the Next.js server rather than exposing it directly, so malformed requests, slow-connection attacks, payload limits and rate limiting never reach the renderer.

A Git repo and a pipeline

Two of the settings below are fixed at build time, not at runtime. If builds happen on someone's laptop, they will drift. Step 7 covers automating it.

Step by step

Deploy Next.js in 7 steps

Steps 1 to 4 get one instance live. Steps 5 and 6 are what a second instance needs. Step 7 stops all of it from drifting.

Build the app and run it

Confirm the scripts, install from the lockfile, build, start. If this works on one machine, every Next.js feature works; nothing further is needed to make the app functional.

bash
npm ci                 # lockfile-exact install
npm run build          # next build
npm run start          # next start, listens on :3000

Pick the output shape

Three options. Deploy the built repo as-is and run next start; or set output: 'standalone', which traces every file the app actually loads into .next/standalone with a minimal server.js that runs without installing node_modules; or set output: 'export' for static files. Standalone is the right shape for containers because it keeps the image small.

The step everyone forgets: the minimal server does not copy public or .next/static, because the docs assume a CDN serves them. Copy them in yourself and server.js serves them automatically.

next.config.js
module.exports = {
  output: 'standalone',
}
bash
next build
cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
node .next/standalone/server.js

Containerize it

Docker deployments support all Next.js features, which includes Kubernetes, Cloud Run, ECS and anything else that runs an image. Build from the standalone output and the runtime layer carries no package manager at all.

Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/public ./public
COPY --from=build /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

Server-side environment variables are read at request time during dynamic rendering, so one image can be promoted through staging and production with different values. Anything prefixed NEXT_PUBLIC_ is the exception: it is inlined into the JavaScript bundle at build time and is baked into the image.

Put a proxy in front, and keep streaming unbuffered

nginx buffers upstream responses by default, which holds a streamed response until it is finished. Server Components, PPR and Server Actions all rely on streaming, so this is the single most common reason a self-hosted app feels slower than the same code on a managed platform while behaving correctly.

next.config.js
module.exports = {
  async headers() {
    return [{
      source: '/:path*{/}?',
      headers: [{ key: 'X-Accel-Buffering', value: 'no' }],
    }]
  },
}

Check the rest of the path too. Load balancers must support chunked transfer encoding or HTTP/2 streaming, and the docs call out AWS ALB with Lambda integration as a default buffer. Without end-to-end streaming, Partial Prerendering still renders correctly but loses the time-to-first-byte advantage that is its entire point.

Pin the build-time identity

Next.js encrypts Server Function closure variables with a key generated per build. Two instances built separately hold two keys, and a Server Function encrypted by one cannot be decrypted by the other: that is the Failed to find Server Action error. Set the key at build time, once, for the release.

bash
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=$KEY next build
# base64, AES key length: 16, 24 or 32 bytes (32 is the default)

Then set a deploymentId. It adds ?dpl=<id> to static assets and an x-deployment-id header to client navigations; when a client and server disagree, Next.js forces a full page reload instead of failing on a missing chunk. This is what makes a rolling deploy survivable.

next.config.js
module.exports = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
}

Share the cache between instances

By default the ISR and data cache lives in memory (50 MB) and on the local disk of each instance. On Kubernetes, every pod holds its own copy, and revalidateTag() on one pod invalidates only that pod. The others keep serving stale content until they discover the change themselves.

next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // disable the per-instance memory cache
}

The handler implements get, set and revalidateTag against shared storage such as Redis or S3. For multi-instance App Router deploys, also implement refreshTags(): it runs before each request and syncs tag state from shared storage, which is how an invalidation on one instance reaches the rest promptly.

Automate the whole flow

Two of the settings above are fixed when next build runs, and the standalone copy step is a build step. That makes reproducibility a pipeline problem, not a server problem: the same commit has to produce the same key, the same deployment id and the same file layout every time. The next section is a worked pipeline.

What actually goes wrong

Six pitfalls, in the order teams hit them

None of these are missing features. Every one is a default that is correct for one instance and wrong for two.

🗂️

Missing CSS and images in standalone

The minimal server.js does not copy public or .next/static. The app boots and renders unstyled. Copy both into the standalone tree, or point a CDN at them with assetPrefix.

🔁

Stale pages after a revalidation

revalidateTag() hits one instance. Every other instance keeps its own on-disk cache and its own answer. A shared cacheHandler plus refreshTags() is the fix; nothing about ISR itself is broken.

🔑

"Failed to find Server Action"

Different builds, different encryption keys. It surfaces as an intermittent error that follows the load balancer, which makes it look like a network fault. Pin NEXT_SERVER_ACTIONS_ENCRYPTION_KEY.

⚠️

Broken navigation mid-deploy

During a rolling deploy the client asks for chunks the new pods no longer have. Without a deploymentId, Next.js cannot tell skew from a genuine 404, so it fails instead of reloading.

🐢

Streaming that arrives all at once

The page is correct and the waterfall is wrong: one long wait, then everything. That is a buffer somewhere in nginx, the load balancer or a second proxy, not a Next.js setting.

✂️

The static export cliff

output: 'export' is a different product, not a smaller one. It removes ISR, Proxy, Server Actions, rewrites, redirects, headers, cookies, Draft Mode and default image optimization in one go.

Automate the whole flow

Build it in a pipeline, deploy it to your host

Next.js gives you a server; the host is your decision. What sits between them is CI/CD, and that layer is where the build-time values above stop drifting. Buddy fits here: it builds the app, then ships the image or the standalone bundle to the infrastructure you already run.

Node builds without runner setup

The Node.js actions run npm ci and next build in a pinned container, so the build environment matches production instead of matching a laptop.

🔑

Build-time secrets, set once

Encrypted variables put NEXT_SERVER_ACTIONS_ENCRYPTION_KEY and the deployment id in one place, so every instance of a release shares them and no key lands in the repo.

🐳

Image build and registry push

Docker actions build the standalone image and push it to any registry, then Kubernetes actions roll it out.

🎯

Deploy to the host you chose

Not a container shop? Remote deployments ship the built output over SSH, rsync or to a cloud target, and roll back when a deploy goes wrong.

Cache the build, not the output

Persisting node_modules and .next/cache between runs keeps incremental builds fast without carrying stale artifacts into the image.

🔀

A URL per branch

Environments stand up a running copy per branch or pull request, which is how you catch a streaming or cache-handler problem before it reaches production.

A minimal Next.js pipeline is three actions: build, image, roll out.

buddy.yml
- pipeline: "Build & deploy Next.js"
  trigger_mode: ON_EVERY_PUSH
  ref_name: refs/heads/main
  actions:
    - action: "Build"
      type: "BUILD"
      docker_image_name: "library/node"
      docker_image_tag: "22"
      execute_commands:
        - "npm ci"
        - "npm run build"
        - "cp -r public .next/standalone/"
        - "cp -r .next/static .next/standalone/.next/"
      cached_dirs:
        - "/buddy/node_modules"
        - "/buddy/.next/cache"   # incremental builds stay fast
    - action: "Build image"
      type: "DOCKERFILE"         # push to your registry
    - action: "Roll out"
      type: "..."                # Kubernetes, SSH, cloud target: your host

Where it can run

Next.js deployment options, compared

Support levels below are the ones stated in the Next.js documentation, not a vendor claim.

OptionFeature supportISR and on-demand revalidationRuns whereWhat you configure
Node.js server All per instance by default Any host that runs Node Proxy, streaming headers, shared cache once you scale out
Docker container All per pod by default Kubernetes, Cloud Run, ECS, any registry Standalone output plus the copy step, then the same multi-instance settings
Static export Limited S3, nginx, Apache, GitHub Pages Almost nothing, because almost nothing runs
Verified adapters Test suite Vercel, Bun Nothing: the adapter owns it
Other integrations Varies varies Cloudflare, Netlify, AWS Amplify, Firebase App Hosting, Deno Deploy, Appwrite Sites Per provider: not built on the public Adapter API

Verified adapters are open source, run the full compatibility test suite and are hosted under the Next.js GitHub organization. Cloudflare and Netlify are working on verified adapters; their current integrations are not verified by the Next.js team. Compiled September 2026 from Next.js 16.3.4 documentation.

Primary sources: Deploying · Deploying to platforms · Self-hosting · Static exports

Go deeper

Primary docs worth reading next

Everything on this page comes from these. No aggregators, no listicles.

The honest take

When one instance is enough, and when it is not

One next start is genuinely fine if…

You run a single server with persistent local disk. The on-disk ISR cache works, revalidateTag() has nothing to coordinate with, the encryption key never differs from itself, and there is no version skew because there is one version. Add a reverse proxy, turn off buffering, and you are done. Plenty of production Next.js apps never need anything past step 4.

Plan for the second instance if…

You autoscale, run rolling deploys, or use ephemeral compute where the disk does not survive. Then steps 5 and 6 are not optional hardening, they are correctness. And if the app leans on PPR shells served at CDN latency or sub-second revalidation propagation, that is what the docs call performance fidelity, the part platforms differentiate on: self-hosting gets you every feature, but matching that profile means building the shared cache and edge layer yourself.

Common questions

Common questions about deploying Next.js

Can you deploy Next.js without Vercel?

Yes. The Next.js documentation states that to run Next.js your platform needs a Node.js server, and that a single next start process handles every feature correctly, including Server Components, ISR, PPR, Cache Components, Server Actions, Proxy and after(). The only extra dependency is the sharp package for Image Optimization. Additional infrastructure such as CDN caching, edge compute and a shared cache improves performance and multi-instance consistency rather than unlocking features.

What is output: 'standalone' in Next.js and when should you use it?

Setting output: 'standalone' in next.config.js makes the build trace every file the app actually needs and copy them into .next/standalone, together with a minimal server.js that replaces next start and runs without installing node_modules. It is the right shape for Docker images because it keeps them small. The catch is that the minimal server does not copy the public or .next/static folders, so you have to copy them into standalone/public and standalone/.next/static yourself after the build, or serve them from a CDN.

Why does a self-hosted Next.js app show stale pages after revalidateTag?

Because the cache is per instance. Next.js stores generated pages and data on the local filesystem of each server instance, so on Kubernetes every pod holds its own copy, and calling revalidateTag() on one instance only invalidates that instance. The other instances keep serving stale content until they discover the invalidation independently. The fix is a custom cacheHandler backed by shared storage such as Redis, cacheMaxMemorySize set to 0 to disable the in-memory layer, and a refreshTags() implementation that syncs tag state before each request.

What causes the "Failed to find Server Action" error after a deploy?

Next.js encrypts Server Function closure variables and generates a unique encryption key for each build. If several instances run different keys, a Server Function encrypted by one instance cannot be decrypted by another and the request fails with "Failed to find Server Action". Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build time to a base64 value with a valid AES key length of 16, 24 or 32 bytes, and use the same key for every instance of the same release.

Does Next.js streaming work behind nginx?

Only if buffering is off. nginx buffers upstream responses by default, which holds a streamed response until it is complete. The documented fix is to send the X-Accel-Buffering: no header, which you can add for every route through the headers() function in next.config.js. The rest of the path matters too: load balancers must support chunked transfer encoding or HTTP/2 streaming, and some, such as AWS ALB with Lambda integration, buffer by default. Without end-to-end streaming, Partial Prerendering still renders correctly but loses its time-to-first-byte advantage.

What do you lose with a Next.js static export?

Setting output: 'export' writes a fully static site to the out folder that any web server can host, but it drops every feature that needs a request at runtime. The documented list of unsupported features covers Dynamic Routes with dynamicParams set to true, Dynamic Routes without generateStaticParams(), Route Handlers that rely on Request, cookies, rewrites, redirects, headers, Proxy, Incremental Static Regeneration, Image Optimization with the default loader, Draft Mode, Server Actions and Intercepting Routes. Route Handlers must also set dynamic to force-static and only GET is supported.

Is Middleware still supported in Next.js 16?

It was renamed. In version 16.0.0 Middleware is deprecated and renamed to Proxy, and Proxy defaults to the Node.js runtime, so the runtime config option is no longer available and throws if used. The codemod npx @next/codemod@canary middleware-to-proxy . performs the rename. Proxy works self-hosted with zero configuration under next start and in Docker, but it is not supported in a static export because it needs access to the incoming request.

Ship it on every push

Build Next.js in a pipeline, deploy it anywhere

One build, the same keys and the same layout every time, shipped to the host you already run.

Get started free