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 --frozen-lockfile
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"]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:
FROM statementsFROM node:lts AS buildFROM 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.
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-lockfile is 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 --offline installs only production dependencies using the cached packages from the previous step. The --offline flag ensures we use only cached packages.
COPY . .
RUN pnpm buildAfter copying the source code, we build the application using pnpm.
FROM node:lts AS runtimeFROM 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.
Cache mounts are one of the most powerful features for optimizing Docker builds with Depot. This Dockerfile uses the following cache mount syntax:
RUN --mount=type=cache,target=/pnpm/store \
pnpm fetch --frozen-lockfiletype=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.