Optimal Dockerfile for Node.js with pnpm
Below is an example Dockerfile that we recommend at Depot for building Docker images for Node applications that use pnpm as their package manager.
# syntax=docker/dockerfile:1
FROM node:lts AS build
RUN corepack enable
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /app
COPY pnpm-lock.yaml ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm fetch
COPY package.json ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile --prod --offline
COPY . .
RUN pnpm build
FROM node:lts AS runtime
RUN groupadd -g 1001 appgroup && \
useradd -u 1001 -g appgroup -m -d /app -s /bin/false appuser
WORKDIR /app
COPY --from=build --chown=appuser:appgroup /app ./
ENV NODE_ENV=production \
NODE_OPTIONS="--enable-source-maps"
USER appuser
ENTRYPOINT ["node", "server.js"]Explanation of the Dockerfile
This Dockerfile uses an optimized multi-stage build approach that leverages pnpm's features for efficient dependency management and caching. We use Node.js LTS and implement security optimizations.
At a high level, here are the things we're optimizing in our Docker build for a Node.js application with pnpm:
- Multi-stage builds via multiple
FROMstatements - pnpm cache mounts for dependency caching
- Offline installation for improved reliability
- Security optimizations with non-root users
Stage 1: FROM node:lts AS build
FROM node:lts AS build
RUN corepack enable
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /appWe start with the Node.js LTS image as our build stage base. We enable corepack to use pnpm without manual installation, and we set up the proper environment variables for pnpm's home directory.
Production dependency installation
COPY pnpm-lock.yaml ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm fetch --frozen-lockfile
COPY package.json ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile --prod --offlineWe copy the lockfile first to leverage Docker's layer caching. The installation process uses two optimized commands:
-
pnpm fetch --frozen-lockfileis a pnpm feature that fetches packages from the lockfile into the pnpm store without installing them. This optimizes the Docker layer cache. -
pnpm install --frozen-lockfile --prod --offlineinstalls only production dependencies using the cached packages from the previous step. The--offlineflag ensures we use only cached packages.
Building the application
COPY . .
RUN pnpm buildAfter copying the source code, we build the application using pnpm.
Stage 2: FROM node:lts AS runtime
FROM node:lts AS runtime
RUN groupadd -g 1001 appgroup && \
useradd -u 1001 -g appgroup -m -d /app -s /bin/false appuser
WORKDIR /app
COPY --from=build --chown=appuser:appgroup /app ./
ENV NODE_ENV=production \
NODE_OPTIONS="--enable-source-maps"
USER appuser
ENTRYPOINT ["node", "server.js"]The runtime stage uses the Node.js LTS image and creates a non-root user for security. We copy the entire built application from the build stage, setting appropriate ownership.
Understanding BuildKit Cache Mounts
Cache mounts in this Dockerfile speed up builds by persisting the package manager cache. This means that even when a layer needs to be rebuilt, your package manager only fetches what's new or updated. This Dockerfile uses the following cache mount syntax:
RUN --mount=type=cache,target=/pnpm/store \
pnpm fetch --frozen-lockfileCache Mount Parameters Explained
type=cache: Specifies this is a cache mount. The cache persists across builds and is managed by BuildKit (and Depot's distributed cache system).target=/pnpm/store: The mount point inside the container where pnpm's store is located. Unlike npm, pnpm uses a content-addressable store that can be shared efficiently across projects.
For more information regarding pnpm cache mounts, please visit the official pnpm documentation.