Next.js Dockerfile with Standalone Output (Alpine, Non-Root)
This Next.js Dockerfile uses three stages: install dependencies, build the app, and run the self-contained standalone server on node:24-alpine. Only the traced files Next.js needs at runtime are copied, so node_modules from the build never ships. The server runs as the built-in node user on port 3000.
- CI status
- Build passing
- Image size (uncompressed)
- 210.4 MB
- Build time
- 45 s
- Runtime image
node:24-alpine- Port
- 3000
- Last built
- 2026-10-15
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:24-alpine 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-alpine 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 produces .next/standalone.
- Commit package-lock.json, because npm ci refuses to run without it.
- Keep a public/ directory in the project, even an empty one, because the runner stage copies it.
How this Dockerfile works
FROM node:24-alpine AS deps RUN apk add --no-cache libc6-compatThe deps stage installs packages. libc6-compat helps native modules built for glibc run on Alpine's musl libc.
RUN npm ciInstalls exactly what the lockfile specifies. Because only package.json and package-lock.json were copied first, this layer is cached until dependencies change.
COPY --from=deps /app/node_modules ./node_modulesThe builder stage reuses the installed modules instead of installing them again, then copies the source and runs the production build.
ENV NEXT_TELEMETRY_DISABLED=1Turns off anonymous Next.js telemetry during the build. The runner stage sets it again for the server process.
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0HOSTNAME=0.0.0.0 is the important one. Without it the standalone server listens only on localhost inside the container and the published port appears dead.
COPY --from=builder --chown=node:node /app/.next/standalone ./The standalone folder holds server.js plus the minimal node_modules Next.js traced. --chown gives the node user ownership without an extra layer.
COPY --from=builder --chown=node:node /app/.next/static ./.next/staticStatic chunks are not included in the standalone output, so they are copied separately. Skipping this line produces pages without CSS or JavaScript.
USER node EXPOSE 3000 CMD ["node", "server.js"]Switches to the unprivileged user that ships with the Node image and starts the standalone server directly with node, without npm in between.
Build and run
docker build -t nextjs-app .
docker run --rm -p 3000:3000 nextjs-appBuild from the project root, then open http://localhost:3000. Public environment variables prefixed with NEXT_PUBLIC_ are inlined at build time, so pass those as build arguments, not at docker run.
Other Next.js Dockerfile variants
Standalone output on Alpine is the leanest common way to self-host Next.js and is the pattern the Next.js team documents. Variants for pnpm, Bun or a Debian slim base suit teams with those toolchains or with native dependencies that dislike musl. They appear below once verified.
| 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
- The build did not produce standalone output. Add output: 'standalone' to next.config, rebuild, and confirm .next/standalone exists locally after npm run build.
- The page loads but has no styling and the browser reports 404s for /_next/static files
- The .next/static folder was not copied into the runner stage. Keep that COPY line, and check that .dockerignore is not excluding it before the build runs.
- The container starts but nothing answers on the mapped port
- The server is probably bound to localhost. Make sure HOSTNAME=0.0.0.0 is set in the runner stage and that you publish the same port the PORT variable uses.
FAQ
- Why are there three stages instead of two?
- Separating dependency installation from the build means a code-only change reuses the cached deps layer. The final stage then copies only runtime output, so build tooling never ships.
- Do I need sharp for image optimization?
- Recent Next.js versions use sharp automatically when it is installed. If next/image is slow or warns in production, add sharp as a dependency and rebuild the image.
- Can I use this with the App Router and Server Actions?
- Yes. Standalone output supports both routers. Runtime secrets used by server code should be passed as environment variables when the container starts.