Deployment, runtimes and self-hosting

Next.js · Expert · 15 min

Production behaviour (caching, scaling, streaming) depends on how and where the app is deployed.

Deployment targets:

  • Node.js server: next build then next start. All features work. Put a reverse proxy (such as nginx) in front for TLS, compression and slow-client protection.
  • Docker: set output: 'standalone' so the build produces a minimal server (.next/standalone/server.js) with only the needed node_modules; copy public/ and .next/static next to it.
  • Static export: output: 'export' writes plain HTML/JS to out/ for any static host, but drops server features (actions, runtime rendering, image optimisation by default, Proxy).
  • Platforms: Vercel supports everything natively; other platforms integrate through deployment adapters.

Runtime: pages, Route Handlers and Proxy run on Node.js. The older Edge runtime (export const runtime = 'edge') is deprecated, and with Cache Components the runtime export isn't supported at all, so remove it when you meet it in older code.

Caching across instances: prerendered HTML lives on disk and 'use cache' entries in memory per instance. With several instances, configure a shared cache handler (cacheHandlers config, 'use cache: remote') and a stable deployment ID and NEXT_SERVER_ACTIONS_ENCRYPTION_KEY.

`after(fn)` from next/server runs work after the response is sent (logging, analytics) without delaying users.

Version skew: clients on an old build may request assets or actions from a new one; a consistent deploymentId lets Next.js detect this and hard-reload.

Example

# Dockerfile (standalone output)
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build                     # next.config.ts has output: 'standalone'

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

# app/api/checkout/route.ts - log after responding
# import { after } from 'next/server'
# export async function POST(req: Request) {
#   const order = await createOrder(await req.json())
#   after(() => analytics.track('order', { id: order.id }))   // runs after the response
#   return Response.json(order, { status: 201 })
# }

Remember

  • next start on Node.js supports everything; output: 'standalone' suits containers
  • output: 'export' produces static files but drops server features
  • Multi-instance setups need a shared cache handler, deployment ID and stable action key
  • after() runs non-critical work after the response

Watch out for

  • Running several self-hosted instances with the default in-memory cache and per-build keys, causing inconsistent caches and failed actions.

Check yourself

  1. Which config makes next build produce a minimal server.js with only the required node_modules for Docker?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nextjs.org/docs/app/guides/self-hosting