pnpm には Docker でキャッシュを利用しやすくする fetch というコマンドが用意されています
この記事では pnpm fetch を使ってキャッシュを利用しやすい Dockerfile を書いていく方法を紹介します
Docker のマルチステージビルドとキャッシュ
Docker にはマルチステージビルドという機能が存在し、単一の Docker イメージ下で実行するのではなく、ビルドやインストール, 本番実行等に分けて Docker イメージ を作成できます
Dockerfile でマルチステージビルドを行う際の例を示します
ARG NODE_VERSION=18.15.0 FROM node:$NODE_VERSION-buster as builder # build 処理 RUN pnpm build FROM node:$NODE_VERSION-slim as runner COPY --from=builder /app/node_modules /app/node_modules COPY --from=builder /app/dist /app/dist CMD ["node", "dist/index.js"]
使い方としてはこんな感じですね
ステージを分ける目的としては、一般的に
- ビルドステップでのみ必要な依存関係(devDependency)やバンドルする場合は node_modules 全体等を本番で使うコンテナに持って行きたくない
- ビルドステップではフルイメージを利用したいが、runner ではできるだけ軽量にしたいため slim イメージを利用する
- ネットワーク転送が原因で時間のかかる処理が複数あるときに、並列で実行したい (COPY 元が前のステージになっていない、かつ
DOCKER_BUILDKIT=1
が設定されているとき並列で実行されます)
等があります
このステージ分けはキャッシュの単位としても機能していて、COPY 元のファイルがキャッシュキーになります
つまり
FROM node:$NODE_VERSION-buster as installer COPY package.json pnpm-lock.yaml ./ RUN pnpm i --frozen-lockfile RUN pnpm build FROM node:$NODE_VERSION-buster as builder # ...
の場合は、package.json と pnpm-lock.yaml に変更が入らなければ installer ステップはキャッシュを利用してスキップできることになります
ですので、pnpm に限らず npm や yarn 等の他のパッケージマネージャーを利用している場合でも package.json とロックファイルを installer としてステージを分離してあげるとキャッシュが適用されやすくなります
pnpm fetch
pnpm fetch は pnpm-lock.yaml から依存関係の取得するコマンドです
上の見出しで installer を分離するテクニックを紹介しましたが、pnpm fetch を使うことで、さらに package.json をキャッシュキーから取り除き、pnpm-lock.yaml のみをキャッシュキーとして依存関係をダウンロードできます
設定ファイルとして package.json を利用するツールも(最近は減った気がしますが)ありますし、scripts も package.json に書かれます
pnpm-lock.yaml のみから取得できると純粋に依存関係が変わらない限りキャッシュを利用し続けられる、ということになります
公式ドキュメントの Dockerfile が動かない
ここからが本記事の本題です
上記の説明は 公式ドキュメントの pnpm fetch のページ にサンプルの Dockerfile とセットで説明されています
公式の推奨するサンプルは以下の通りです
FROM node:14 RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm # pnpm fetchはロックファイルのみが必要 COPY pnpm-lock.yaml ./ # パッケージにパッチを当てた場合は、pnpm fetchを実行する前にパッチを含める COPY patches patches RUN pnpm fetch --prod ./ RUN pnpm install -r --offline --prod EXPOSE 8080 CMD [ "node", "server.js" ]
しかしながら、これをそのまま手元で動かすと上手く行きませんでした
No projects found in "/app"
といわれてインストールされず、node_modules 以下には.modules.yaml
と.pnpm
のみ作成される- おそらく package.json がないことで install 時にプロジェクトを認識できていないことが原因なので、空の package.json を生成してみる
- → 指定なしのときは pnpm-lock.yaml が空になる
- frozen-lockfile でインストールしてみる
- エラーになる
pnpm fetch 自体は意図通り動いていますが、おそらくバージョンアップ等で package.json がない状態での pnpm install -r --offline --prod
が実行できなくなったものと思われます
fetcher step と builder step に分離する
install が動かなかったので、回避策として fetch, install と build ではなく、fetch と install, build に分離しました
node_modules 以下への展開は install で行われますが、fetch の時点でダウンロードは完了しているのでここまでキャッシュできれば install も十分高速に実行されることが期待できます
分離後の Dockerfile は以下の通りです
ARG NODE_VERSION=18.15.0 FROM node:$NODE_VERSION-buster as fetcher WORKDIR /app COPY pnpm-lock.yaml /app RUN corepack enable pnpm RUN corepack prepare pnpm@8.2.0 --activate RUN pnpm fetch --prod ./ FROM node:$NODE_VERSION-buster as builder WORKDIR /app COPY --from=fetcher /root/.local/share/pnpm/store/v3 /root/.local/share/pnpm/store/v3 COPY --from=fetcher /app/node_modules /app/node_modules COPY --from=fetcher /app/pnpm-lock.yaml /app/pnpm-lock.yaml COPY package.json /app RUN corepack enable pnpm RUN corepack prepare pnpm@8.2.0 --activate RUN pnpm i --offline --prod --frozen-lockfile FROM node:$NODE_VERSION-slim as runner # ...
pnpm fetch すると仮想ストアにダウンロードされるので仮想ストアが格納されるパスもコピーしてきます
パスは以下のようにして調べられます
$ docker run node:18.15.0-buster bash -c "corepack enable pnpm && corepack prepare pnpm@8.2.0 --activate && pnpm store path" Preparing pnpm@8.2.0 for immediate activation... /root/.local/share/pnpm/store/v3
これで「依存関係が変わらない限り依存関係のダウンロードをキャッシュする」ことができるようになりました
まとめ
このエントリでは pnpm fetch を使うことで、依存関係のダウンロードをキャッシュしやすい Dockerfile について紹介しました
install 処理は --offline
であっても package.json がないと動かないので fetch までで STAGE を分離し、node_modules と Virtual Store をコピーすることで対応しました
pnpm をお使いの方はぜひお試しください