Understanding Next.js Docker Images

Understanding Next.js Docker Images

Jonas Scholz - Co-Founder von sliplane.ioJonas Scholz
5 min

The first time you read a Next.js Dockerfile, it can look like several Dockerfiles stacked on top of each other. There is a reason for that: installing dependencies, building the app, and serving it need different files.

Four parts, one final image

The original example divided the Dockerfile into a base image, dependency installation, build, and runtime:

The four parts of the original Next.js Dockerfile

The diagram shows the original Node 18 example. The complete Dockerfile below uses Node 24 and npm, so you can copy the current example instead of transcribing the screenshot.

First, enable standalone output in your existing Next.js configuration:

// next.config.mjs
const nextConfig = {
  output: "standalone",
};

export default nextConfig;

Keep any other configuration your project needs. Standalone output traces the server's dependencies and places the required files in .next/standalone. We will run its server.js directly.

Dockerfile
FROM node:24-bookworm-slim AS base
WORKDIR /app

FROM base AS dependencies
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS build
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN mkdir -p public && npm run build

FROM base AS runtime
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=build --chown=node:node /app/public ./public
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]

This assumes an npm project with a committed package-lock.json and a build script that runs next build. Add a .dockerignore before building:

node_modules
.next
.git
.env
.env.*
npm-debug.log

The base image

All stages start from the same Node image. Here I use Debian slim to avoid mixing libc implementations when dependencies include native code. Alpine can also work, but check native dependencies rather than adding a compatibility package blindly.

The major version in the tag is not a permanent pin. Rebuild images to get fixes, and use a digest when your release process requires an exact base image.

Dependency installation

The dependency stage copies the package manifest and lockfile, then runs npm ci. It happens before copying the source so editing a page does not force Docker to reinstall unchanged packages.

The original Next.js example supported npm, Yarn, and pnpm by checking which lockfile existed. You only need the branch for the package manager your project actually uses. Keep one package manager and its lockfile consistent between development and the container build.

Building the app

The build stage receives node_modules from the dependency stage, then adds the source and runs npm run build. The mkdir -p public ensures the later copy also works for projects without a public directory.

Build-time values must be available at this point. In particular, NEXT_PUBLIC_ values are included in the client bundle; setting a different value when the container starts will not rewrite that bundle. Keep secrets out of public variables and the client code.

Running the app

The final stage copies three things: the standalone server, .next/static, and public. Standalone output does not copy those static directories for you. Missing them can leave a page without its JavaScript, styles, or images.

COPY --chown=node:node gives the runtime user ownership of these files, including the .next directory used by runtime caches. USER node then runs the server without root privileges.

HOSTNAME=0.0.0.0 is important. Binding the server to localhost would prevent requests through the container's network interface. PORT=3000 sets the listening port, while EXPOSE only documents it.

The final command starts the standalone server directly. It does not use next start or run another build.

Check the result

docker build -t next-app .
docker run --rm --name next-app -p 3000:3000 next-app

Open http://localhost:3000. Check a page, a static asset, and a server route if your app has one. A successful HTML response alone does not prove the static files were copied correctly.

The official Next.js Docker example is a useful reference for more complex setups. Monorepos may need output tracing settings and a different server path. Once your image works locally, deploy the repository on Sliplane with HTTP port 3000.

Welcome to your cloud platform

Sliplane makes it simple to deploy and scale your apps in the cloud. Start with a container or your favorite framework and grow from there. Try it now and get started in minutes!