We use cookies to understand how people use Depot.
Container Builds

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 --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"]

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 FROM statements
  • 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 /app

We 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 --offline

We copy the lockfile first to leverage Docker's layer caching. The installation process uses two optimized commands:

  1. 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.

  2. 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.

Building the application

COPY . .

RUN pnpm build

After 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 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-lockfile

Cache 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.