NestJS Dockerfile with pnpm for Production (Alpine, Multi-Stage)

This NestJS Dockerfile installs dependencies with pnpm through Corepack, compiles the TypeScript build, prunes development packages, and ships only dist, production node_modules and package.json on node:24-alpine. The API runs as the unprivileged node user on port 3000. It suits most REST and GraphQL NestJS services.

CI status
Build passing
Image size (uncompressed)
181.1 MB
Build time
20 s
Runtime image
node:24-alpine
Port
3000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build && pnpm prune --prod

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 RUN corepack enable

    Corepack ships with Node and provides the pnpm version pinned in package.json, so you don't need a global npm install of pnpm.

  2. COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile

    Copying only the manifests first caches the install layer. --frozen-lockfile fails the build instead of silently updating versions if the lockfile is out of date.

  3. RUN pnpm run build && pnpm prune --prod

    Compiles TypeScript into dist, then removes development dependencies such as the Nest CLI and TypeScript, so only runtime packages remain to be copied.

  4. FROM node:24-alpine

    The final stage starts from a clean image. Corepack and pnpm aren't needed here, because the app is started with node directly.

  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 the compiled output, owned by the node user so the app can read them without root.

  6. USER node

    Runs the API as the unprivileged user built into the official Node image instead of root.

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

    Starts the compiled entry point directly. Without npm or pnpm as a parent process, shutdown signals reach Nest and its shutdown hooks run cleanly.

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 any route on http://localhost:3000 to confirm the container answers.

Other NestJS Dockerfile variants

pnpm on Alpine gives fast, cache-friendly installs and a lean runtime, so it's a good default for teams already using pnpm. npm or Yarn variants and a Debian slim base for native modules appear below once they pass CI.

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

Troubleshooting

ERR_PNPM_OUTDATED_LOCKFILE Cannot install with frozen-lockfile
package.json changed without updating the lockfile. Run pnpm install locally, commit the updated pnpm-lock.yaml, and rebuild.
Error: Cannot find module '/app/dist/main.js'
Your build writes to a different path, for example dist/src/main.js. Check nest-cli.json and tsconfig, then point the CMD line at the real compiled entry file.
A package that was a devDependency is missing at runtime
pnpm prune --prod removed it. Move the package to dependencies in package.json if your application imports it while running.

FAQ

Why use Corepack instead of npm install -g pnpm?
Corepack reads the packageManager field and activates exactly that pnpm version. Local machines, CI and the image then all use the same pnpm, without an extra global install layer.
Is Alpine safe for NestJS?
For most NestJS apps, yes. If you depend on native modules compiled for glibc, such as some database drivers or image libraries, a Debian slim base avoids musl compatibility problems.
How do I enable graceful shutdown?
Call app.enableShutdownHooks() in main.ts. Because node runs as the main process, the stop signal from docker stop reaches Nest directly.