Helm7

How to Dockerize Next.js: A Working Guide

The Helm7 team · Published 18 September 2026 · Checked 30 September 2026 · 8 min read

dockerize next.js: container ships moored at a terminal in Hamburg in the evening
Photo by Wolfgang Weiser on Pexels

Most Next.js images come out far bigger than they need to be, because the obvious Dockerfile ships every dev dependency and the whole source tree. To dockerize Next.js properly you turn on standalone output and build in three stages. This guide gives you the files, the commands and the mistakes that make a container build cleanly and then serve nothing, checked against the Next.js 16 docs.

In short

  • To dockerize Next.js well, set output to standalone in next.config. It is the single biggest fix for an oversized image.
  • Build in three stages and start the last one from a fresh base, so only the standalone output ships.
  • Copy public and .next/static yourself, and set HOSTNAME to 0.0.0.0, or the container serves nothing useful.
  • Only NEXT_PUBLIC_ variables are baked in at build time. Read the rest at runtime and one image can serve every environment.
  • Node 20 reached end of life on 30 April 2026. Base new images on Node 24.

Why does the obvious Dockerfile make such a big image?

Most people who dockerize Next.js for the first time write the obvious Dockerfile. It copies the whole project into a Node image, runs npm install, then npm run build. Everything stays in the final layer. That means dev dependencies, the package cache, your test files, and a node_modules tree that the running app mostly never reads.

Next.js has a build mode for this. During next build it traces which files each page can actually load, using static analysis of import, require and fs calls. Set output to standalone and it copies just those files, including the few node_modules it needs, into .next/standalone. That folder can run on its own, without installing anything.

You also get a small server.js that replaces next start. The docs say it can be deployed "on its own without installing node_modules", which is the whole point. It is also what Docker recommends: their own Next.js guide builds a sample this way and shows the result at 130MB.

What to sort out before you write the Dockerfile

Before you dockerize Next.js, three small decisions save you an afternoon. First, a base image. Use a Node line that is still supported. Pin the major version, not "latest", so a rebuild next spring does not change your runtime by surprise.

Node.js release lines on 30 September 2026, from the Node.js release schedule
LineStatus todayMaintenance beginsEnd of life
Node 20End of life22 October 202430 April 2026
Node 22Maintenance21 October 202530 April 2027
Node 24Active LTS20 October 202630 April 2028
Node 26Current, LTS from 28 October 202620 October 202730 April 2029

Node 24 is the sensible pick today. Node 20 reached end of life in April, so if your old Dockerfile still says node:20, that is the first thing to change.

Second, a .dockerignore file. Without it, COPY . . drags your local node_modules, .git and any old .next folder into the build. Docker's guide excludes node_modules, .next, out, .env files and .git. Do the same.

Third, know which variables are baked in. Only names starting with NEXT_PUBLIC_ are inlined into the JavaScript at build time. Everything else is read when the server runs. We come back to that below, because it decides whether one image can serve every environment.

Dockerize Next.js in six steps

  1. 1Turn on standalone outputIn next.config.ts set output to the string standalone. Nothing else in the config needs to change. In a monorepo, also set outputFileTracingRoot to the repository root, or files outside the app folder will not be traced and the container will fail on a missing import.
  2. 2Add a .dockerignoreList node_modules, .next, out, .git and .env followed by a star. It keeps the build context small and stops a stale local build leaking into the image.
  3. 3Write the DockerfileThree stages: install from the lockfile, build, then copy only the standalone output into a clean base. The full file is in the next section.
  4. 4Build itFrom the project root run docker build with a tag. The first build is slow. Later builds reuse the dependency layer until package-lock.json changes.
  5. 5Run it with the port publishedUse docker run with -p 3000:3000. If the page does not load, the cause is nearly always HOSTNAME, covered in the pitfalls.
  6. 6Check the image, not just the pageRun docker images to see the size, and docker run with your production variables to confirm the app reads them at runtime rather than from your laptop.

The three files, in full

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'standalone',
}

export default nextConfig
next.config.ts
node_modules
.next
out
.git
.env*
Dockerfile
npm-debug.log
.dockerignore
# syntax=docker/dockerfile:1
FROM node:24-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:24-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Dockerfile: install, build, then run from a clean base

That is the whole file. Read the runner stage closely. It starts from a fresh image, so nothing from deps or builder survives except what the three COPY lines bring across. The build stage can be as heavy as it likes. Only the runner ships.

The standalone server does not copy public or .next/static by default. The Next.js docs say these "should ideally be handled by a CDN instead". If you have no CDN in front, copy them in as above, or the page loads with no styles and no images.

Why does the container build fine and then serve nothing?

Almost always the server is listening on the wrong interface. By default it binds to localhost, which inside a container means only the container itself can reach it. Set HOSTNAME to 0.0.0.0 and docker run -p works. The Next.js docs show the same form: PORT=8080 HOSTNAME=0.0.0.0 node server.js.

  • Page loads with no CSS or images: public or .next/static was not copied into the runner stage.
  • Container exits at once with "Cannot find module": in a monorepo, outputFileTracingRoot is not set, or a native package such as sharp needs an outputFileTracingIncludes entry.
  • Permission errors writing the cache: the files were copied as root and the app runs as the node user. The --chown flags above fix it.
  • A NEXT_PUBLIC_ value is stale: it was inlined at build time. Changing it means rebuilding the image, not restarting the container.
  • Image is still huge: the runner stage starts from the builder stage instead of a fresh FROM line.
“This allows you to use a singular Docker image that can be promoted through multiple environments with different values.”
Next.js documentation, How to self-host your Next.js application, Environment Variables

That sentence is the reason to keep secrets and URLs out of NEXT_PUBLIC_ names. Read them on the server during a dynamic render and one image can move from staging to production untouched.

A worked example: what image size costs you in pulls

Image size sounds abstract until you multiply it. This is a worked example with assumed inputs, so change the numbers to match yours. Take the 130MB sample image from Docker's guide. Say you ship 20 times a month to 3 servers, and every server pulls the whole image each time.

Worked example: data pulled per month, worst case with no layer reuse
ImageSizePulls per monthData per month
Standalone runner, as in Docker's guide130MB20 deploys x 3 servers = 6060 x 130MB = 7,800MB (about 7.8GB)
Same app at 4 times the size (assumed)520MB6060 x 520MB = 31,200MB (about 31GB)

Real pulls reuse unchanged layers, so the true figure is lower for both. The gap is what matters. A smaller final image also starts faster on a new machine, which shows up when a container restarts at a bad moment.

What changes when you run more than one container?

On a single container, every Next.js feature works, ISR included. The trouble starts with a second copy. Each container keeps its own cache on local disk, so a revalidation on one does not reach the other. The docs also ask for the same build to boot every container, and one shared NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, or Server Actions fail with "Failed to find Server Action" errors.

The caching side is covered in Incremental Static Regeneration when you self-host. If you are choosing between a container and a plain Node server, the comparison of every way to deploy a Next.js app sets them side by side.

Where to run the image

Once you dockerize Next.js, the image runs on a VPS with Docker, on Kubernetes, on Cloud Run, or on a platform that builds it for you. Docker hosting has its own trade-offs, set out in Docker hosting explained. Helm7 builds your repository in a container, starts the new version, waits until it answers, moves traffic across and gives the old one 30 seconds to finish. Running the Dockerfile above locally is still a good way to check that a deploy will behave.

If you would rather not maintain a Dockerfile at all, the self-hosting guide covers the plain Node route.

Common questions

  • Does Docker support Next.js features like ISR and streaming?

    Yes. The Next.js docs say Docker deployments support all Next.js features. On one container, ISR, streaming, Server Actions and Proxy behave as they do with next start. Extra setup starts when you run several containers and need them to share a cache.

  • Why is my image still large after I dockerize Next.js with standalone output?

    Check that the final stage starts from a fresh FROM line and copies only .next/standalone, .next/static and public. If it continues from the builder stage, it still carries dev dependencies and your source tree.

  • Do I need a .dockerignore file?

    Yes. Without one, COPY . . sends node_modules, .git and any local .next folder into the build context. That slows the build and can leak a stale local build into the image.

  • Can I dockerize Next.js with the Pages Router?

    Yes. To dockerize Next.js on either router, standalone output and the same Dockerfile structure work for the App Router and the Pages Router. The setting lives in next.config either way.

  • Which Node version should the image use?

    Use a supported LTS line. On 30 September 2026 that is Node 24, or Node 22 until its end of life on 30 April 2027. Node 20 is already out of support.

Sources

  1. Next.js docs: output (standalone and file tracing)
  2. Next.js docs: How to self-host your Next.js application
  3. Next.js docs: Deploying
  4. Docker Docs: Containerize a Next.js application
  5. Node.js release schedule

Run your entire app stack, end to end, from $12 a month.

App, Postgres, auth, storage, jobs and domains on one platform, planned and priced before anything changes.

More from the blog