Deploying Next.js
Run a Next.js app four ways, a Node.js server, Docker, a static export or a platform's adapter, and see which features each one supports.
- 01Name the four ways to deploySay what runs when a request comes in for a Node.js server, a Docker container, a static export and a platform with an adapter.
- 02Read what each way supportsUse the docs' tables to tell which features need a server, which need streaming, and which need a cache that every server shares.
- 03Choose a way for a projectMatch a project's needs, like cookies, regenerated pages or no servers to run, to the way of deploying that fits.
One tea shop, four places to run it
Deploying an app means putting it on a computer that visitors can reach, and keeping it running there. For a Next.js app, it starts the same way every time: next build turns your project into the files a server or a host needs. What runs those files is your choice. The Next.js docs name four ways:
- A Node.js server. Any computer that runs Node.js runs
next start, Next.js's own server. - A Docker container. The same server, packed with Node.js and your app into one box that runs the same way everywhere.
- A static export. No server of yours at all:
next buildwrites plain files, and any web host sends them. - A platform with an adapter. A hosting company runs the app for you. Its adapter is code that reshapes the build for its computers.
Here is the tea shop, built once and run the first way. It has the kinds of pages earlier lessons built: a home page with a photo, a menu that's cached for an hour, a cart that reads a cookie, and a proxy.ts. This example is a real Next.js 16.4.3 project. It was built with next build, started with next start, and what the server sent back was recorded. Click a file in the tree to read it.
app/page.tsx1import Image from "next/image";2 3export default function Home() {4 return (5 <main>6 <h1>Tea shop</h1>7 <Image src="/green-tea.jpg" alt="A cup of green tea" width={480} height={320} />8 <p>Green tea: $4. Mint tea: $3.</p>9 </main>10 );11}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (5/5) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found├ ◐ /cart└ ○ /menu 1h 1d ƒ Proxy (Middleware) ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000✓ ReadyOpen the Terminal tab. Under $ next build, the route table lists every route and how it's made: ○ / and ○ /menu are prerendered as static content, /menu with a Revalidate of 1h, ◐ /cart is a static shell with a part streamed in for each request, and ƒ Proxy (Middleware) says proxy.ts runs on the server. Under $ next start, the server says it's Ready on port 3000. Then the Network tab holds the five answers that one process sent: the pages, the cart made for the request, and a photo the image optimizer resized.
next start, so both support every feature. A static export is only files, so nothing runs when a request comes in. A platform runs the build through its adapter, and supports what it was built to support.A bakery bakes its bread once in the morning, and can sell it four ways. In its own shop, the baker can still bake a fresh loaf for a customer who asks. A food truck carries the same oven anywhere it parks. A supermarket shelf only holds loaves wrapped ahead of time: nobody there can bake to order. A market hall runs the stalls for many bakers, and each stall fits the hall's rules about what it can do.
- In real life: The recipe and the morning's baking
- In Next.js: Your project and
next build - In real life: Your own shop, with an oven behind the counter
- In Next.js: A Node.js server running
next start - In real life: A food truck: the same oven, packed to park anywhere
- In Next.js: A Docker container
- In real life: Wrapped loaves on a supermarket shelf, no oven in sight
- In Next.js: A static export: files only
- In real life: A market hall that runs the stalls, with its own rules
- In Next.js: A platform, and its adapter
You build a Next.js app once, then choose what runs it. A Node.js server and a Docker container run next start, so they support every feature. A static export is only files, so only what was made during the build works. A platform runs your build through its adapter, and supports what that platform supports.
A Node.js server: next build, then next start
The first way is the one every project already has. Click package.json in the example's tree, one of the files marked create-next-app. Lines 7 and 8 are its build and start scripts: next build and next start. On any computer with Node.js, these three commands install, build and run the shop:
npm ci
npm run build
npm run start
Line 1 installs the exact packages the lockfile lists. Line 2 builds the app into the .next folder. Line 3 starts the server, which listens on port 3000 until you stop it (next start -p 8080 picks another port). The docs say this server supports every Next.js feature, and the recording shows several of them working in one process:
- Static pages.
/came withCache-Control: s-maxage=31536000: the page was made during the build, and a shared cache like a CDN may keep it for a year. - Regenerated pages.
app/menu/page.tsxis cached with"use cache"andcacheLife("hours"). Its answer sayss-maxage=3600, stale-while-revalidate=82800: fresh for an hour, then served old while the server makes a new one in the background. That's Incremental Static Regeneration (Incremental Static Regeneration), and only a running server can do it. - Streaming.
/cartcame withprivate, no-cache, no-store…, and its static shell first, withLoading your cart…, then the part that read the cookie (Partial Prerendering). - Proxy.
/teashas no page, yet it answered with the home page:proxy.tsrewrote the request on line 5 before any page ran (Proxy). - Image Optimization. The last answer is the photo as WebP, 640 pixels wide, made by the server on request:
X-Nextjs-Cache: MISS. That needs thesharppackage, whichnpm ciinstalled with Next.js (Optimizing images).
Three more things come with running the server yourself. Environment variables that a page reads at request time are read when the request comes in, so one build can run with different values in each environment (Server-only code and environment variables). after() callbacks run once the answer has gone (Work after the response), so the server must be stopped gracefully: sent SIGTERM or SIGINT, then given time. The docs recommend 10 to 30 seconds, while it finishes the requests in flight and the pending callbacks. And the docs recommend a reverse proxy, like nginx, in front of the server rather than exposing it to the internet directly. Self-hosting sets all of that up.
Docker: the same server in a container
A server needs the right Node.js, the right packages and your built app, set up the same way on every computer that runs it. Docker packs all of that into one file:
- A Dockerfile is a list of steps that sets up the app, one after the other.
docker buildruns the steps and saves the result as an image: a snapshot of a small Linux system with Node.js, the packages and the built app inside.docker runstarts a container from the image: a running copy, kept apart from the computer around it. One image can start many containers, on any computer or cloud that runs Docker, including orchestrators like Kubernetes.
This is the simplest Dockerfile that runs the tea shop. It isn't run on this page, and it isn't the one to ship:
FROM node:24.13.0-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]
Line 1 starts from an image with Node.js already in it, the version the official example uses. Line 2 makes /app the folder every later step works in. Lines 3 and 4 copy only the package files and install from the lockfile. Because they come before the rest of the code, Docker can reuse that step when only your code changes. Line 5 copies the rest of the project, and line 6 runs next build. Line 7 notes the port the server listens on, and line 8 is the command a container runs when it starts: npm run start, so next start. Two commands build the image and start one container:
docker build -t tea-shop .
docker run -p 3000:3000 tea-shop
Line 1 builds the image and names it tea-shop. Line 2 starts a container and connects the computer's port 3000 to the container's, so localhost:3000 reaches the shop. Inside, it's the same server as before, so the docs say a Docker deployment supports every feature too.
This image is bigger than it needs to be. It holds every package, the development tools included, and the whole project. The docs' official example uses output: "standalone", which makes next build write a small folder with only the files the server needs, and copies only that into the last stage of the image. Self-hosting builds that Dockerfile and records what the standalone folder holds. One more note from the docs: while you develop on a Mac or on Windows, run npm run dev directly on your computer rather than in Docker. It's faster there.
A static export: files on any web host
A static export leaves the server out. With output: "export" in next.config.ts, next build writes every page into a folder called out, as files. Any web server that sends files can host them: Amazon S3, nginx, Apache, GitHub Pages. The site you're reading is a Next.js app exported this way.
The price is everything that needs code to run when a request comes in. Try it with a smaller tea shop: a home page, and a cart. Each button below is one version, and each was built with real Next.js 16.4.3. Before you click, guess which ones build.
next.config.ts and its cartNext.jsnext.config.ts1import type { NextConfig } from "next";2 3const nextConfig: NextConfig = {4 /* config options here */5 cacheComponents: true,6 partialPrefetching: true,7};8 9export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (4/4) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ◐ /cart ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentNo output option: the build is for a server. The cart reads its cookie on the server, so the build prints ◐ /cart, and the Network tab shows the cart made for one request.
Here is what each version shows:
- For a server. The cart's
CartItemreads the cookie on line 5 ofapp/cart/page.tsx, insideSuspense. The build prints◐ /cart, and the server makes that part for each request. - Export, cookie on the server. The build stops on
/cart: it couldn't be rendered statically because it usedcookies(). In an export, every page must be made during the build, and no cookie exists then. - Export, cookie in the browser. The cart now reads the cookie in a Client Component, after the page has loaded:
app/cart/cart-item.tsx, shown below. All four pages prerender. Then the export itself stops:Invariant: PPR cannot be enabled in export mode. In Next.js 16.4.3, a static export can't have Cache Components on. - Export, Cache Components off. With
cacheComponentsandpartialPrefetchingremoved, the build prints○ /cartand writesout:index.html,cart.htmland404.html,.txtfiles that hold each page's RSC payload for the router, and the JavaScript in_next/static. That folder is the whole site.
"use client";
import { useEffect, useState } from "react";
export default function CartItem() {
const [tea, setTea] = useState<string | null>(null);
useEffect(() => {
setTea(document.cookie.match(/(?:^|; )cart=([^;]*)/)?.[1] ?? "");
}, []);
if (tea === null) return <p>Loading your cart…</p>;
return <p>{tea ? `In your cart: ${tea}` : "Your cart is empty."}</p>;
}
Line 1 makes CartItem a Client Component (Client Components). Line 6 starts with null, so the page made during the build shows Loading your cart… on line 10. The Effect on lines 7 to 9 runs in the browser and reads the cookie from document.cookie, and line 11 shows it. Every visitor gets the same file; the browser fills in their own cart.
The static exports guide lists what isn't supported, because it needs a server: cookies and other request-time APIs, proxy.ts, rewrites, redirects and headers from next.config.ts, Incremental Static Regeneration, Image Optimization with the default loader, Draft Mode, Server Actions, intercepting routes, Route Handlers that read the request, and dynamic routes whose addresses generateStaticParams doesn't list (generateStaticParams). Static exports, in this stage, goes further: images, single-page apps, and what to do instead of each.
Platforms and adapters
A platform is a hosting company that runs your app on its computers, so you don't run servers yourself: Vercel, Netlify, Cloudflare, AWS Amplify, Firebase App Hosting, Deno Deploy and others. A platform often splits an app into parts: functions that run on demand, files on its CDN, rules that send each request to the right part. Something has to turn next build's output into those parts. That's an adapter.
next build and reshapes its output for one platform: code that renders, files its CDN serves, the rules that send each request to the right place. Where cached pages and data live is the other half, configured with cacheHandler and cacheHandlers.Next.js has a public Build Adapters API for this, stable since Next.js 16.2. An adapter is a module named by adapterPath in next.config.ts, or by the NEXT_ADAPTER_PATH environment variable, which lets a platform set it for you:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
adapterPath: require.resolve("./my-adapter.js"),
};
export default nextConfig;
Line 6 names the adapter. It runs during next build and writes what its platform needs. The docs list two kinds of platform support:
- Verified adapters are open source and run Next.js's full compatibility test suite, and live under the Next.js GitHub organization. On the docs' Deploying page, those are Vercel's and Bun's. Vercel's adapter uses the same public API as everyone else's.
- Other platforms have their own Next.js integrations, not built on the public API and not verified, so what works varies: the docs name Appwrite Sites, AWS Amplify Hosting, Cloudflare, Deno Deploy, Firebase App Hosting and Netlify. Cloudflare and Netlify are working on verified adapters.
The docs separate two ideas. Functional fidelity means every feature works correctly; the test suite checks it. Performance fidelity means a feature is as fast as it can be, like a static shell served from a CDN close to the visitor. Platforms differ most in the second. Adapters and other platforms, later in this stage, looks at Vercel, Netlify, Cloudflare and OpenNext on AWS in turn.
Which features each way supports
The docs' Deploying page sums up the four ways in one table. Each row is one way:
| Way to deploy | What runs when a request comes in | Features |
|---|---|---|
| A Node.js server | next start, which npm run start runs, after next build. | All. |
| A Docker container | The same next start (or the standalone server.js), inside a container. | All. |
| A static export | Nothing of yours. A web server sends files from the out folder. | Limited: nothing that needs a server. |
| A platform's adapter | Whatever the platform runs, made from the build by its adapter. | It varies. Verified adapters run Next.js's own test suite. |
Most features' pages in the docs end with a Platform support table with the same four columns. Here they are side by side. Image Optimization, Partial Prerendering, Server Actions and Route Handlers have no such table, so their rows come from the Deploying page and the self-hosting, static exports and PPR platform guides. Each row is one feature, and Platform-specific is the docs' word for "ask your platform":
| Feature | Node.js server | Docker container | Static export | Adapters |
|---|---|---|---|---|
| Incremental Static Regeneration | Yes. | Yes. | No. | Platform-specific. |
| Image Optimization with the default loader | Yes, with sharp installed. | Yes, with sharp installed. | No. Use a custom loader, or unoptimized. | Depends on the platform. |
proxy.ts | Yes. | Yes. | No. | Platform-specific. |
Cache Components ("use cache") | Yes. | Yes. | No. | Platform-specific. |
| Partial Prerendering (a static shell, then streamed parts) | Yes: next start sends the shell and renders the rest in one response. | Yes. | No. In 16.4.3 the export stops while Cache Components is on. | Depends on the platform. Some serve the shell from a CDN and resume at the server. |
Streaming (loading.tsx, Suspense) | Yes. | Yes. | No. | Platform-specific. |
| Server Actions | Yes. | Yes. | No. | Depends on the platform. |
| Route Handlers | Yes, every method. | Yes, every method. | Only GET handlers made into files during the build (dynamic = "force-static"), none that read the request. | Depends on the platform. |
after() | Yes, if the server is stopped gracefully. | Yes, if the container is stopped gracefully. | No. | Platform-specific. |
The pattern is the one the playground showed: what needs code at request time works wherever next start runs, and not in a static export. Two details matter once there's more than one server, or something between the server and the visitor. The docs' Deploying to Platforms guide has a matrix for them. Each row is one feature: does it need responses to stream through, and should every instance of the server share one cache?
| Feature | Needs streaming | A cache shared by every instance |
|---|---|---|
| Server Components | Required. | No. |
| ISR, after a time | No. | Recommended. Each instance works alone without one. |
ISR, on demand (revalidateTag) | No. | Recommended. A tag expired on one instance must reach the others. |
| Partial Prerendering | Required. | Recommended. |
| Cache Components | Required. | Recommended. |
| Proxy | No. | No. |
| Server Actions | Required. | No. |
after() | No. It needs a graceful shutdown. | No. |
Streaming means the answer can arrive in pieces, like the cart's shell and then its cookie part. Nothing between the server and the browser may hold the pieces back until the end (Streaming). A host that does still works, the docs say, but the visitor waits for the whole page. A shared cache matters with several instances: without one, a page regenerated on one instance stays old on the others.
Partial Prerendering has one more option. next start sends a route's static shell and renders the rest in the same response. A CDN can also keep the shell close to visitors and ask the server for only the rest: the docs call this resuming, a POST with the header next-resume: 1 and the shell's saved state as its body. It's only faster, never required. Every feature in these tables works on a single next start.
Choosing a way for each project
Each row is one project, what it needs, and a way that fits it:
| Project | What it needs | A good fit |
|---|---|---|
| The tea shop | A cart that reads a cookie, a menu that's regenerated, photos resized on request. | A Node.js server or a Docker container, or a platform whose adapter supports these features. |
| A tea blog on a host that only sends files | Posts written ahead of time. Nothing about one visitor on the server. | A static export. In Next.js 16.4.3, with Cache Components off. |
| A company that runs every app in Kubernetes | The same container image in every environment, many copies at once. | A Docker container, built with the standalone output. |
| A small team with nobody to run servers | No servers to patch or watch. | A platform, ideally with a verified adapter. |
The first question is always the same: does anything need code to run when a request comes in? Sort these needs of the tea shop:
- The cart page reads the visitor's cookie in a Server Component.
- The menu is made again in the background once its hour has passed.
proxy.tssends the old/teasaddress to the home page./_next/imageresizes the photo for each screen.- The home page lists teas and reads nothing from the request.
- A Client Component reads the cart cookie in the browser.
- A dynamic route whose every address is listed by
generateStaticParams. - Photos come from an image service through a custom loader.
Sort each need: does it need a server running when the request comes in, or can a host that only sends files serve it?
Common mix-ups
- "Next.js only runs on Vercel." No. The docs' first way is any computer with Node.js, and it supports every feature. Vercel is one platform, with one adapter, built on the same public API as the others.
- "Docker supports fewer features than a server." No. The container runs the same server. What it changes is how the server is packed and started.
- "A static export is a smaller build of the same app." No. It's a different kind of app: no code of yours runs when a request comes in. The playground's builds stopped on the cookie, and on Cache Components.
- "A static export can't have anything per visitor." It can, in the browser. The exported cart reads its cookie in a Client Component, after the file arrives.
- "Any platform supports everything." Not necessarily. The docs say support varies; verified adapters run the test suite, so you can see what works.
- "Features need a CDN or edge servers." No. The docs say one
next starthandles every feature correctly. A CDN, a shared cache and edge servers make it faster, and keep many instances in step.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
This is the tea shop's package.json, as create-next-app wrote it. Which two scripts put the shop on a Node.js server?
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.4.3",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
npm run build, then npm run start. Line 7 runs next build, which writes the app into .next. Line 8 runs next start, the server the recording started: ✓ Ready on port 3000.
This cart page is built with output: "export". The build stops. Which function does its error name?
import { Suspense } from "react";
import { cookies } from "next/headers";
async function CartItem() {
const tea = (await cookies()).get("cart")?.value;
return <p>{tea ? `In your cart: ${tea}` : "Your cart is empty."}</p>;
}
export default function CartPage() {
return (
<main>
<h1>Your cart</h1>
<Suspense fallback={<p>Loading your cart…</p>}>
<CartItem />
</Suspense>
</main>
);
}
cookies(). The recorded build printed: Route /cart with dynamic = "error" couldn't be rendered statically because it used cookies(). In a static export, every page is made during the build, and no request, so no cookie, exists then.
With this cart and Cache Components off, the export builds. Which file in out does a host send when someone asks for /cart?
"use client";
import { useEffect, useState } from "react";
export default function CartItem() {
const [tea, setTea] = useState<string | null>(null);
useEffect(() => {
setTea(document.cookie.match(/(?:^|; )cart=([^;]*)/)?.[1] ?? "");
}, []);
if (tea === null) return <p>Loading your cart…</p>;
return <p>{tea ? `In your cart: ${tea}` : "Your cart is empty."}</p>;
}
out/cart.html. The recorded export lists it in out, beside index.html and 404.html. A host sends that file for /cart, and this CartItem reads the cookie in the browser afterwards.
On the Node.js server, which Cache-Control header does /menu come with?
import { cacheLife } from "next/cache";
export default async function MenuPage() {
"use cache";
cacheLife("hours");
return (
<main>
<h1>Menu</h1>
<p>Mint tea: $3. Green tea: $4.</p>
</main>
);
}
s-maxage=3600, stale-while-revalidate=82800. One hour is 3,600 seconds; one day minus one hour is 86,400 - 3,600 = 82,800. The recording's /menu answer has exactly this header, and the build lists the route with 1h and 1d.
The tea shop's owner writes a small tea blog. Posts change once a week, nothing depends on who's reading, and it will live on a school's web host that only sends files. Which way of deploying fits?
A static export. The posts are written ahead of time and nothing reads the request, so every page can be a file in out. The school's host only sends files, which is all an export needs. In Next.js 16.4.3, turn Cache Components off for it: with it on, the recorded export stopped with Invariant: PPR cannot be enabled in export mode.
A host stops the shop's container one second after sending it SIGTERM, every time it deploys a new version. Answers already sent reach their visitors. Which Next.js feature can lose work?
after(). Its callbacks run once the answer has gone. The docs say the server finishes requests in flight and pending after() callbacks when it's sent SIGTERM, and recommend letting it drain for 10 to 30 seconds. One second may cut them off.
Check your understanding
For each question, ask what runs when a request comes in, and where.
Question 1 of 8Which two ways of deploying does the docs' Deploying page say support every Next.js feature?
Choose an answer to see the explanation.
Question 2 of 8What does a Docker container change about which features work?
Choose an answer to see the explanation.
Question 3 of 8The tea shop's cart page reads a cookie with
cookies(). What happens when you build it withoutput: "export"?Choose an answer to see the explanation.
Question 4 of 8In Next.js 16.4.3, the cart now reads its cookie in the browser, and
next.config.tsstill hascacheComponents: truewithoutput: "export". What does the build print?Choose an answer to see the explanation.
Question 5 of 8What is an adapter, in the docs' Build Adapters API?
Choose an answer to see the explanation.
Question 6 of 8Which of these does the docs' feature matrix say needs streaming responses?
Choose an answer to see the explanation.
Question 7 of 8What does
after()need from the server it runs on?Choose an answer to see the explanation.
Question 8 of 8What makes an adapter "verified"?
Choose an answer to see the explanation.
Key takeaways
- You build once with
next build. Then a Node.js server, a Docker container, a static host or a platform runs the result. next startsupports every feature: static and regenerated pages, streaming,proxy.ts, the image optimizer, Cache Components andafter(). A Docker container runs the same server.- A static export writes
out, a folder of files. Nothing of yours runs per request, so cookies, ISR, proxy, Server Actions and the default image loader don't work there. In Next.js 16.4.3, it also needs Cache Components off. - A platform runs the build through its adapter (
adapterPath). Verified adapters are open source and run Next.js's test suite; others vary. - With several instances, the docs recommend a shared cache, and streaming must pass through every proxy.
after()needs a server that's stopped gracefully.
Remember the one-liner.
Build once, then ask what must run per request: if anything does, a server runs it; if nothing does, files are enough.
These links point at release v16.4.3, the version every example on this page was built with. The error the third playground version hit is thrown in lines 903 to 907 of export/index.ts, when a static export finds cacheComponents on. The adapter types live in build/adapter/build-complete.ts. The docs' official Docker example is examples/with-docker.
Coming next: Self-hosting. It runs the shop on servers you manage: the standalone output, a production Dockerfile, a CDN in front, a cache shared by many instances, and deployments that don't break open pages.