NestJS Dockerfile with npm for Production (Alpine, Non-Root)

This NestJS Dockerfile installs dependencies with npm ci, compiles your TypeScript, prunes development packages and ships only dist, production node_modules and package.json on node:24-alpine. The API runs as the non-root node user on port 3000. It's the default choice for projects created with the Nest CLI and npm.

CI status
Build passing
Image size (uncompressed)
181.4 MB
Build time
14 s
Runtime image
node:24-alpine
Port
3000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
.dockerignore
.git
node_modules/
dist/
.env*
coverage/
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM node:24-alpine AS build

    The build stage holds the Nest CLI, TypeScript and every other dev dependency. None of it reaches the final image.

  2. COPY package.json package-lock.json ./ RUN npm ci

    Installing from the lockfile before copying source caches dependencies, so code changes skip the slow install step.

  3. RUN npm run build && npm prune --omit=dev

    Compiles TypeScript into dist, then removes devDependencies so only runtime packages remain to be copied.

  4. FROM node:24-alpine

    The runtime stage starts clean, with only Node itself before the build output is copied in.

  5. COPY --from=build --chown=node:node /app/node_modules ./node_modules COPY --from=build --chown=node:node /app/dist ./dist

    Copies the pruned dependencies and compiled code, owned by the node user, so the app can read them without root.

  6. USER node

    Runs the API as the unprivileged user that ships with the official Node image.

  7. CMD ["node", "dist/main.js"]

    Starts the compiled entry point directly. With node as the main process, shutdown signals reach Nest's shutdown hooks.

Build and run

terminal
docker build -t nestjs-app .
docker run --rm -p 3000:3000 nestjs-app

Pass database URLs and secrets with -e flags or an env file at run time. Then call a route on http://localhost:3000 to confirm the container answers.

Other NestJS Dockerfile variants

npm is the default package manager for new Nest projects, so this variant needs no extra setup. Teams using pnpm can use the pnpm variant, which installs faster and caches better. Both are compared in the table below.

VariantRuntime imageImage sizeCI
Node Alpine + npmnode:24-alpine181.4 MBPassing
Node Alpine + pnpmnode:24-alpine181.1 MBPassing

Troubleshooting

npm ci can only install packages when your package.json and package-lock.json are in sync
The lockfile is outdated. Run npm install locally, commit the updated package-lock.json, and rebuild the image.
Error: Cannot find module '/app/dist/main.js'
Your build writes elsewhere, for example dist/src/main.js. Check nest-cli.json and tsconfig, then update the path in the CMD line.
A devDependency is missing at runtime
npm prune removed it. Move any package your running app imports from devDependencies to dependencies.

FAQ

Why prune instead of a second npm ci --omit=dev?
Pruning reuses the packages already downloaded for the build, which is faster than a second install. Both approaches end with the same production dependencies.
Should I switch to pnpm?
Only if your team already uses it. pnpm installs faster and saves disk space, but npm needs no extra setup in the image.
How do I enable graceful shutdown?
Call app.enableShutdownHooks() in main.ts. Because node runs as the main process, docker stop delivers the signal directly to Nest.