pnpmを使ったNode.js Dockerマルチステージビルドの最適化とレイヤー軽量化

Docker pnpm Node.js DevOps
結論

BuildKitキャッシュマウントと pnpm --prod deploy を組み合わせ、最終イメージへ本番アセットのみをコピーします。

# pnpm公式仕様:マルチステージ Dockerfile
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME/bin:$PATH"
RUN corepack enable

FROM base AS build
COPY . /app
WORKDIR /app
-- pnpm store のキャッシュを有効化
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build

-- 本番用パッケージの抽出
FROM base AS deployer
WORKDIR /app
COPY --from=build /app /app
RUN pnpm --filter=my-app --prod deploy /pruned

-- 最終本番イメージ
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deployer /pruned .
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

pnpm公式推奨:pnpm deploy の仕組み

pnpm公式ドキュメントで解説されている pnpm deploy コマンドは、モノレポや単一プロジェクトから devDependencies(ビルドツール類)を完全に削ぎ落とし、指定のディレクトリ(例: /pruned)に実行に必要な本番アセットだけを集約・コピーする機能です。


実際に起こる事故:pnpm deploy が失敗する罠

1. pnpm-workspace.yaml の設定漏れ

モノレポ環境で pnpm deploy を実行する際、pnpm-workspace.yamlinject-workspace-packages: true を記述していないと、ローカルパッケージの依存関係解決に失敗してビルドがストップします。

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
inject-workspace-packages: true

2. Corepack のストアキャッシュ未適用によるビルド低速化

RUN --mount=type=cache を記述しない場合、Dockerビルドのたびに全パッケージがネットワーク経由で再ダウンロードされ、ビルド時間が数分単位に肥大化します。


最適化手順

  1. Dockerfileの各ステージで Corepack を有効化し pnpm コマンドを使用可能にする
  2. RUN --mount=type=cache,id=pnpm,target=/pnpm/store でローカルビルドキャッシュを保持する
  3. 最終ランタイムステージに COPY --from=deployer /pruned . で最小構成のアセットのみを移植する