Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Deployment

server.js Not Included in npm run build: What Next.js Produces, What You Have to Ship, and Where It Actually Lives

Sean

Platform Writer

Jun 17, 2026
11 min read

next build does not produce a self-contained deployable artifact. It produces a .next/ directory that depends on your source files, your node_modules, your next.config.js, and a runtime, plus a start command that knows where those pieces live. There is no server.js in the build output unless you explicitly turn on output: 'standalone', and even then the file the standalone mode produces is generated, not the one you wrote. That is the practical answer. The frustrating answer is that the docs assume you are deploying to Vercel, where the platform fills in all the gaps. This post is for the engineer who has to deploy to something else — a Docker container, a VM, a PaaS that is not Vercel — and wants to understand what the build output actually is before betting the deploy on it.

This post assumes you have shipped a Next.js app to Vercel, that you have hit a wall trying to deploy it somewhere else, and that you want the version of the mental model that lets you debug the next wall without a Stack Overflow thread.

server.js not included in npm run build: what Next.js produces, what you have to ship, and where it actually lives

Table of contents

What next build actually produces

next build produces a .next/ directory. The directory contains compiled JavaScript bundles, server-side rendering output, static page HTML, image optimization caches, build manifests, and webpack chunk graphs. It does not contain your source code, your node_modules, your next.config.js, or your package.json. It does not contain a server.js.

The structure that matters:

  • .next/server/ — compiled server-side code, including the App Router runtime, the API route handlers, and the server components.
  • .next/static/ — the static assets that the browser fetches: _next/static/chunks/... JavaScript, CSS, fonts, images.
  • .next/standalone/ — present only if output: 'standalone' is set in next.config.js. A self-contained Node deployment with a server.js entry point and a minimal node_modules.
  • .next/cache/ — webpack’s cache, the swc compiler cache, the type-checker cache. Safe to delete in production; useful to keep in CI for faster rebuilds.
  • .next/required-server-files.json — the manifest of files that the runtime needs to find on disk.
  • .next/BUILD_ID — a hash that identifies this specific build. Useful for cache busting and for “did the deploy actually replace the old version?” checks.

The build output is not a directory you can node directly. It is a directory that the Next.js runtime reads from, plus the runtime itself.

Why there is no server.js in the default output

The reason is that the default next build output is designed for Vercel. On Vercel, the platform provides the runtime, the source files, the node_modules, the next.config.js, and the start command. The platform also handles the routing, the CDN, the image optimization, the edge runtime, and the function isolation. The build output is the parts of the application that are unique to this build; everything else is provided by the platform.

For a non-Vercel target, the engineer is the platform. The engineer has to provide the runtime, the source files, the node_modules, the next.config.js, and the start command. The build output is still just the parts unique to the build, but the parts the platform used to provide have to be assembled by hand.

This is why the docs feel opaque to anyone deploying outside Vercel. The docs assume the platform is filling in the gaps. The gaps are invisible until the engineer has to fill them in.

The four files that have to ship together

The minimum set of files for a non-Vercel Next.js deployment:

  • .next/ — the build output.
  • node_modules/ — the runtime dependencies. Production-only; use npm ci --omit=dev or npm prune --production to keep the size down.
  • package.json — so the runtime knows the dependencies and the scripts. The start script is what the platform will run.
  • next.config.js (or .mjs) — the configuration the runtime reads at start time. Many features (image optimization, redirects, headers, env var prefixes) live here.

The optional fifth:

  • public/ — the static files served at the root of the site. Images, favicons, robots.txt, anything that should be served as-is.

The deploy pattern:

  1. Run npm ci to install dependencies.
  2. Run next build to produce .next/.
  3. Copy .next/, node_modules/, package.json, next.config.js, and public/ to the production server.
  4. Run npm start (which is next start) on the production server.
  5. The platform routes traffic to the listening process.

The five steps assume the source is the same on the build server and the production server. The pattern breaks when the source is on the build server and not on the production server — which is why many teams use Docker to ship the build server as a unit.

The output: standalone mode that produces a server.js

The mode that produces a self-contained server.js is output: 'standalone':

// next.config.js
module.exports = {
  output: 'standalone',
};

With this flag, next build produces a .next/standalone/ directory that contains:

  • server.js — the entry point. Run it with node server.js and the app starts.
  • A minimal node_modules/ — only the packages the runtime actually imports, traced by Next.js’s build pipeline. No devDependencies, no test frameworks, no documentation tools.
  • package.json — required for the runtime to resolve some modules.
  • .next/ — the build output, copied into the standalone directory.

The advantage is that the standalone directory is a self-contained deployable unit. Copy it to a container, run node server.js, and the app starts. No npm ci on the production server, no node_modules to manage, no next.config.js to remember.

The trade-off is that the standalone output is not the same as the default output. Some features (the image optimizer, the edge runtime, certain experimental flags) may behave differently. The output also does not include public/; that has to be copied separately.

The deploy pattern with standalone:

next build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
# optional, only if next.config.js is needed at runtime:
cp next.config.js .next/standalone/

# build a Docker image from .next/standalone
docker build -t myapp:latest -f Dockerfile.standalone .

The Dockerfile for a standalone build is short:

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

The COPY --from=builder assumes a multi-stage build. The builder stage runs next build with output: 'standalone'. The runner stage copies the standalone output and runs node server.js. The result is a small image, a small attack surface, and a single start command.

The start command that matches each deployment target

The start command depends on the deployment target. The default is next start, which runs the bundled Next.js server on port 3000. The non-defaults are common.

Vercel. The platform runs next start automatically. The start command is part of the platform configuration, not the project.

Docker (standalone). CMD ["node", "server.js"]. The standalone output’s server.js is the entry point.

Docker (non-standalone). CMD ["npm", "start"] or CMD ["npx", "next", "start"]. The full Next.js runtime is in the image; npm start invokes it.

Systemd / VM. npm start with a working directory that contains .next/, node_modules/, package.json, and next.config.js. A node process supervisor (PM2, systemd, supervisord) keeps the process alive.

PM2. pm2 start npm --name myapp -- start or pm2 start "node server.js" --name myapp for standalone. The ecosystem.config.js can specify environment, instances, and memory limits.

Kubernetes. The container runs node server.js or npm start; the Pod’s command and args are set in the manifest. A liveness probe hits / to confirm the process is alive. A readiness probe hits / after a longer delay to confirm the runtime has finished warming up.

PaaS like RunxBuild. The platform reads the start command from the service configuration. npm start, node server.js, next start, and a custom command are all first-class options. The platform manages the process lifecycle, the health checks, the restarts, and the logs.

The trap is that the start command has to match the build output. If the build is standalone, the start command is node server.js. If the build is not standalone, the start command is next start (which requires node_modules and next.config.js on the production server). Mismatching the two is the most common reason a Next.js deploy fails on a non-Vercel target.

The Dockerfile that actually works

The Dockerfile that works on a non-Vercel target is the multi-stage one that builds inside the container:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]

The build stage runs npm ci and npm run build with the full node_modules (including devDependencies for the build). The production stage copies only what the runtime needs: the standalone output, the static assets, and the public/ directory. The NODE_ENV=production flag tells Next.js to skip telemetry calls. The NEXT_TELEMETRY_DISABLED=1 flag is belt-and-suspenders.

The size of the resulting image is around 150 MB. The build takes 60-180 seconds depending on the app size. The runtime starts in 1-3 seconds.

For a non-standalone build (which keeps the full node_modules in the production image), drop the .next/standalone lines and use CMD ["npm", "start"]:

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY .next ./.next
COPY public ./public
COPY next.config.js ./
EXPOSE 3000
CMD ["npm", "start"]

This image is bigger (around 400 MB with the production node_modules) and slower to build (because npm ci --omit=dev runs at image-build time, not at app-build time). It is the right pick when the build is not standalone, or when the team wants the full Next.js runtime available for debugging.

Why Vercel hides all of this (and what you lose when you leave)

Vercel hides all of this because the platform fills in every gap. The build command is next build. The output directory is .next/. The install command is npm install. The start command is next start. The platform provides the Node runtime, the source code, the node_modules, the next.config.js, the routing, the CDN, the image optimization, the edge runtime, and the function isolation. The engineer ships the app code; the platform ships the infrastructure.

What you lose when you leave Vercel:

  • The build cache. Vercel caches node_modules, the .next/cache, and the build artifacts across deploys. Self-hosted CI has to manage the cache explicitly or accept slower builds.
  • The image optimization. Vercel’s image optimizer is a platform service. Self-hosted Next.js has to configure the image optimizer (or use a third-party service like Cloudinary or imgix).
  • The edge runtime. Vercel runs the edge runtime on its edge network. Self-hosted Next.js does not have an edge network unless the team builds one.
  • The preview deployments. Vercel creates a unique URL for every PR. Self-hosted CI has to build and deploy the preview manually.
  • The function isolation. Vercel runs API routes and server components as isolated functions. Self-hosted Next.js runs them as a single Node process.

The losses are real, but they are also the things a team takes on explicitly when they leave Vercel. The trade-off is cost (self-hosted can be cheaper at scale) or control (self-hosted can be configured in ways Vercel does not allow) or compliance (self-hosted can run inside a VPC the platform cannot reach).

For teams that want Vercel’s deployment experience without Vercel’s price tag, a PaaS like RunxBuild offers a middle ground. The platform handles the runtime, the routing, the CDN, the environment variables, and the health checks. The engineer ships the app code. The trade-off is that the platform does not optimize every Next.js feature, but it does optimize the ones most teams actually use.

The trap with custom server.js files

A Next.js app can have a custom server.js file in the project root:

// server.js (in the project root, not the build output)
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
  }).listen(3000, () => {
    console.log('> Ready on http://localhost:3000');
  });
});

This is a common pattern for apps that need custom routing, custom middleware, or a non-Next.js request handler. The trap is that this server.js is not the server.js that output: 'standalone' produces.

  • The custom server.js lives in the project root. It is the entry point the developer wrote. It calls next() and uses Next.js as a request handler.
  • The standalone server.js lives in .next/standalone/. It is generated by the build. It is the full Next.js server, no developer code.

A team that has a custom server.js and enables output: 'standalone' will end up with two server.js files. The custom one in the root; the generated one in .next/standalone/. The custom one will not be used by the standalone build. The team has to either:

  • Drop the custom server.js and use the standalone build’s server.
  • Keep the custom server.js and run it as the entry point, with the full Next.js runtime available.

The decision depends on whether the custom code is needed at runtime. If the custom code is needed (custom middleware, custom routing, custom request handling), keep it. If not, drop it and use the standalone build.

The Docker command for a custom-server.js app:

COPY server.js ./
CMD ["node", "server.js"]

The full Next.js runtime has to be in the image. The standalone build is not the right pick.

The deploy checklist for non-Vercel targets

A checklist for any non-Vercel Next.js deploy:

  • The build command matches the start command. next build with output: 'standalone' produces .next/standalone/server.js; the start command is node server.js. next build without standalone produces .next/; the start command is npm start (which runs next start). Mixing them produces a deploy that fails to start.
  • The runtime version is pinned. node:20-alpine in the Dockerfile, 20.11.1 in the platform’s runtime config, 20.11.1 in .nvmrc. Pin the patch version to make builds reproducible.
  • The public/ directory is copied. The build output does not include public/. The production server needs it.
  • The .next/static/ directory is copied. The build output’s static chunks have to ship with the runtime.
  • The NODE_ENV=production env var is set. Tells Next.js to skip development-only features (the dev overlay, the source maps, the telemetry).
  • The port is configured. Next.js defaults to 3000. Some platforms (Heroku, Cloud Run) set PORT via an env var. The start command has to honor it: next start -p $PORT or node server.js -p $PORT.
  • The health check endpoint is wired. / returns 200 once the runtime is warm. Most platforms need a 200 response to consider the deploy healthy.
  • The logs are streamed to stdout. Next.js logs to stdout by default. The platform’s log collector reads stdout. Make sure the start command does not redirect stdout to a file.
  • The NEXT_PUBLIC_* env vars are set at build time. The build command has access to them. The runtime does not (for client-side code). Build-time env vars are baked into the JS bundle; runtime env vars are read by the server only.

The checklist is mechanical. The discipline is doing it every time.

The opinion this post is built on

The opinion is that Next.js is a perfectly good framework for non-Vercel deployments, but the documentation is written for the Vercel deployment. The mental model the docs assume (the platform fills in the gaps) does not apply when the engineer is the platform. The work of deploying Next.js outside Vercel is the work of becoming the platform: providing the runtime, the source, the node_modules, the next.config.js, the start command, the routing, the CDN, the logs.

A useful exercise: try output: 'standalone' on the next Next.js deploy. The standalone output is the cleanest abstraction the framework offers for non-Vercel targets. The Dockerfile fits on a screen. The start command is one line. The image is small. The deploy is fast. The path off Vercel becomes a config decision, not a rewrite.

FAQ

Why is server.js not included in npm run build?

next build does not produce a self-contained server bundle. It produces a .next/ directory that depends on node_modules, next.config.js, and the Next.js runtime. There is no server.js in the default output. To get a server.js, enable output: 'standalone' in next.config.js.

Where does Next.js put the build output?

The build output is in the .next/ directory at the project root. The directory contains the compiled server code (.next/server/), the static assets (.next/static/), the build manifests, and (if standalone mode is enabled) a .next/standalone/ directory with a server.js entry point.

What is output: 'standalone' in Next.js?

A build configuration in next.config.js that produces a self-contained .next/standalone/ directory. The directory contains a server.js entry point, a minimal node_modules/ with only the runtime dependencies, and the build output. The standalone output is deployable as a single unit to Docker, a VM, or a PaaS.

How do I deploy a Next.js app to Docker?

The standard pattern is a multi-stage Dockerfile. The build stage runs npm ci and next build (with output: 'standalone' for the smallest image). The production stage copies .next/standalone/, public/, and .next/static/ from the builder, and runs node server.js.

What is the start command for a Next.js app?

The default is npm start, which runs next start. For a standalone build, the start command is node server.js. For a custom server.js file in the project root, the start command is node server.js (referring to the developer’s file, not the standalone output).

Does Next.js need a different Dockerfile for SSR vs SSG?

The Dockerfile is the same. The build output differs. SSR apps produce a .next/server/ directory with the server-side rendering runtime. SSG apps produce a .next/server/pages/ directory with pre-rendered HTML. Both deploy the same way.

Can I deploy Next.js without Vercel?

Yes. The platform-agnostic deploy targets are Docker (with the standalone build), a VM with Node.js installed, a PaaS like RunxBuild that handles the runtime, or Kubernetes. Each target has the same four files to ship: .next/, node_modules/, package.json, next.config.js (plus public/).

Why does Vercel make Next.js deploys easier?

Vercel fills in every gap. The platform provides the Node runtime, the source code, the node_modules, the next.config.js, the routing, the CDN, the image optimization, the edge runtime, and the function isolation. The engineer ships the app code. Self-hosted deploys have to provide the gaps.

What is the difference between next start and node server.js?

next start runs the Next.js CLI’s start command, which reads .next/, node_modules/, and next.config.js from the current directory. node server.js runs the standalone output’s server.js file, which is a pre-built entry point that does not need node_modules or next.config.js on the production server.

What happens if I forget to copy public/ to the production server?

Images, favicons, and other static assets served from /public/ return 404. The Next.js runtime does not serve the public directory; it expects it to be on disk. The deploy will start and respond to requests, but the public assets will be missing.

What env vars does Next.js need at build time vs runtime?

NEXT_PUBLIC_* env vars are baked into the JS bundle at build time. The runtime does not see them. All other env vars are read at runtime. The start command runs in the production environment, so the runtime env vars have to be set in the platform’s configuration (not the build environment).

How does RunxBuild deploy Next.js apps?

RunxBuild reads the build command, the output directory, and the start command from the service configuration. For Next.js, the build command is npm run build, the output is .next/, and the start command is npm start (or node server.js for standalone builds). The platform manages the runtime, the environment variables, the health checks, and the logs. The hosting calculator shows the cost of running a Next.js app at a given traffic level.

#server js not included npm run build#nextjs#next build#deployment#nodejs#vercel#docker#ci cd