Next.js Dockerfile on Debian Slim (Standalone, Non-Root)
This Next.js Dockerfile builds standalone output in three stages on node:24-slim, a Debian-based image with glibc, and runs the self-contained server as the node user on port 3000. Pick it over Alpine when your app uses native modules or prebuilt binaries compiled for glibc, such as image processing, Prisma engines or headless browsers.
- CI status
- Build passing
- Image size (uncompressed)
- 268.6 MB
- Build time
- 46 s
- Runtime image
node:24-slim- Port
- 3000
- Last built
- 2026-10-15
# 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 . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"].git
node_modules/
.next/
.env*
npm-debug.log*
Dockerfile
.dockerignoreBefore you build
- Set output: 'standalone' in next.config so the build creates .next/standalone.
- Commit package-lock.json, because npm ci requires it.
- Keep a public/ directory in the project, even if it's empty.
How this Dockerfile works
FROM node:24-slim AS depsThe slim image is Debian with unnecessary packages removed. It uses glibc, so native modules built for standard Linux work without the compatibility shims Alpine needs.
COPY package.json package-lock.json ./ RUN npm ciInstalls exactly what the lockfile pins. Copying only the manifests first keeps this layer cached until dependencies change.
COPY --from=deps /app/node_modules ./node_modulesThe builder stage reuses the installed modules, then copies the source and runs the production build.
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0HOSTNAME=0.0.0.0 makes the standalone server listen on all interfaces. Without it, it binds to localhost and the published port looks dead.
COPY --from=builder --chown=node:node /app/.next/standalone ./Copies server.js and the minimal node_modules Next.js traced during the build, owned by the unprivileged node user.
COPY --from=builder --chown=node:node /app/.next/static ./.next/staticStatic chunks aren't part of the standalone folder. Leaving this out serves pages without their CSS and JavaScript.
USER node EXPOSE 3000 CMD ["node", "server.js"]Runs the server as the built-in node user and starts it with node directly, so shutdown signals reach the process.
Build and run
docker build -t nextjs-app .
docker run --rm -p 3000:3000 nextjs-appBuild from the project root and open http://localhost:3000. Remember that NEXT_PUBLIC_ variables are baked in at build time, so pass them as build arguments rather than at docker run.
Other Next.js Dockerfile variants
Alpine produces the smaller image and suits most apps. The Debian slim variant costs some extra megabytes but avoids musl compatibility problems with native dependencies. Compare the measured image sizes of both in the table below before choosing.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Node Alpine + standalone output | node:24-alpine | 210.4 MB | Passing |
| Node Debian slim + standalone output | node:24-slim | 268.6 MB | Passing |
Troubleshooting
- COPY failed: /app/.next/standalone: not found
- Standalone output isn't enabled. Add output: 'standalone' to next.config and confirm the folder appears locally after npm run build.
- Pages render without styles and /_next/static requests return 404
- The .next/static folder wasn't copied into the runner stage. Keep that COPY line and check .dockerignore doesn't exclude it before the build.
- A native module fails to load with an error about a missing shared library
- The module needs a system library that slim omits. Install it with apt-get in the runner stage, then clean the apt lists in the same layer.
FAQ
- Should I use Alpine or slim for Next.js?
- Start with Alpine for the smaller image. Switch to slim if a dependency ships glibc-only binaries, crashes on musl, or needs libc6-compat workarounds that keep breaking.
- Why three stages instead of two?
- Separating dependency installation from the build lets code-only changes reuse the cached install. The final stage then copies only runtime output, so build tools never ship.
- How do I pass runtime secrets?
- Server-side code reads environment variables when the container starts, so pass secrets with -e or an env file at docker run. Never bake them into the image.