Beyond [“npm start”] : There is [“node”, “server.js”] in Next.js applications.

npm start vs node server.js

When I first started deploying Next.js apps to containers about three years ago, I used to just toss npm start into my Dockerfile, watch the build pass, and call it a day. I thought, “Hey, it works on my machine, it works in the cloud, we’re good!”

But then I actually looked at my image sizes and resource usage. My “simple” app was taking up 1.5GB of space, and the startup time was… let’s just say it gave me enough time to go grab a coffee.

If you want to level up your deployment game (and save a ton of money on cloud storage), we need to talk about why we’re ditching npm start for node server.js using Next.js output tracing.

The “Junior” Way: npm start

When you run npm start, you aren’t just running your app. You’re running the entire npm CLI tool, which then looks at your package.json, which then spawns a child process to run next start.

  1. The Bloat: You’re carrying around node_modules, including all those heavy devDependencies (like Tailwind, TypeScript, and ESLint) that you only needed during the build phase.

  1. The Overhead: Every MB matters when you’re scaling containers. Keeping the full source code and build tools in your production image is like moving into a new apartment but bringing all the empty cardboard boxes with you.

The “Pro” Way: node server.js (Standalone)

Next.js introduced a “standalone” output mode that is a total game-changer for Docker. When you enable this, Next.js figures out exactly which files are needed for production and copies them into a tiny, self-contained folder.

How to flip the switch

In your next.config.js, just add this:

module.exports = {
output: 'standalone',
}

Now, when you run next build, Next.js creates a folder at .next/standalone. This folder contains a minimal server.js file. You can run this with a simple node server.js command—no npm required, and no massive node_modules folder needed.

The Ultimate Minimal Dockerfile

To get that “chef’s kiss” minimal image, we use Multi-stage builds. We build the app in one stage and then copy only the standalone files into a tiny Alpine Linux image.

Feature npm start (Standard) node server.js (Standalone)
Image Size ~1GB – 1.5GB ~100MB – 150MB
Security Higher risk (more binaries) Lower risk (minimal surface area)
Performance Slower startup Faster “cold starts”

The “Better” Dockerfile Structure

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

# Stage 2: Run
FROM node:20-alpine AS runner
WORKDIR /app

# We only copy the standalone folder and static assets
# This is where the magic happens
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"]

Why this matters for us

As devs with a few years under our belts, we start realizing that “working” isn’t the same as “optimized.” Switching to node server.js in a standalone build:

  1. Speeds up CI/CD: Smaller images push and pull to registries way faster.
  2. Saves 💰: Less storage and faster scaling means lower AWS/GCP bills.
  3. Stability: You’re running the exact production-compiled code without the fluff of the package manager.

Trust me, once you see your image size drop from 1.2GB to 120MB, there’s no going back.

Share this article:
Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *