pnpm & Monorepo な環境のマルチステージビルドの書き方
pnpm のドキュメントには Working with Docker というページがあり、Monorepo の例も用意されています。
ただ、この例動かないんですよね。Dockerfile の解説をしつつ、動くように修正していきます。
手っ取り早く最終版を知りたい人は目次から最後まで飛んでください。
GitHub にもコードを置いています。
解説パート
Example 2: Build multiple Docker images in a monorepo
を見ていきます。
※ 前提として、app1
, app2
は common
に依存しているとします。
2 ~ 4 行目
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
Corepack 経由で pnpm をインストールし、後々キャッシュで使うために PNPM_HOME
を設定しています。
5, 6 行目
COPY . /app
WORKDIR /app
全部コピーしています。あくまで例なのでこうなっていると思いますが、実際は頻繁に変更されるものは最後にコピーしましょう。ベストプラクティスは以下のページに書かれています。
8 ~ 13 行目
FROM base AS prod-deps
RUN pnpm install --prod --frozen-lockfile
FROM base AS build
RUN pnpm install --frozen-lockfile
RUN pnpm run -r build
dev-dependencies と dependencies をそれぞれインストールしています。ここで pnpm のキャッシュを使っています。
また、ビルドもしています。
15 行目 ~ 最後
FROM base AS common
COPY /app/packages/common/node_modules/ /app/packages/common/node_modules
COPY /app/packages/common/dist /app/packages/common/dist
FROM common AS app1
COPY /app/packages/app1/node_modules/ /app/packages/app1/node_modules
COPY /app/packages/app1/dist /app/packages/app1/dist
WORKDIR /app/packages/app1
EXPOSE 8000
CMD [ "pnpm", "start" ]
FROM common AS app2
COPY /app/packages/app2/node_modules/ /app/packages/app2/node_modules
COPY /app/packages/app2/dist /app/packages/app2/dist
WORKDIR /app/packages/app2
EXPOSE 8001
CMD [ "pnpm", "start" ]
最終段階として、それぞれのパッケージで必要な dist
フォルダと node_modules
をコピーしてきます。
ここがダメで、pnpm はシンボリックリンクを使って node_modules
を構築します。そのため、node_modules
をコピーする際には、シンボリックリンクを展開する必要があります。
実際にこのままビルドするとリンクのみがコピーされてしまいます。
$ docker build . --target app1 --tag app1:latest
$ docker run --rm -it app1:latest bash
---------
root@xxxxx:/app/packages/app1# ls -la node_modules/
total 12
drwxr-xr-x 2 root root 4096 Oct 16 09:03 .
drwxr-xr-x 1 root root 4096 Oct 16 09:03 ..
lrwxrwxrwx 1 root root 12 Oct 16 09:03 common -> ../../common
lrwxrwxrwx 1 root root 65 Oct 16 09:03 date-fns -> ../../../node_modules/.pnpm/date-fns@2.30.0/node_modules/date-fns
# ../../common 等は存在しない
これを解決するために、今回は pnpm のコマンドである pnpm deploy
を使用します。
改良パート
pnpm deploy
(deploy の方のドキュメントには Monorepo での使用法として書かれているんですよね。)
これを使うと、ドキュメントにある通り node_modules
を含む全ファイルがコピーされます。最終ステージではこのコマンドの結果をコピーするだけで済むようになります。
また、--prod
オプションを使うことで dependencies のみをコピーできます。なので、 pnpm install は1回で済みます。
$ pnpm deploy --prod deploy/app1
$ tree -aL 2 deploy/app1
app1
|-- dist
| `-- main.js
|-- node_modules
| |-- .pnpm
| |-- common -> .pnpm/file+packages+common/node_modules/common
| `-- date-fns -> .pnpm/date-fns@2.30.0/node_modules/date-fns
|-- package.json
|-- src
| `-- main.ts
`-- tsconfig.json
不要な ts ファイルなどを除去する
実行には dist
, node_modules
, package.json (pnpm run をする場合のみ)
が必要なので、これ以外をコピーしないことで容量の削減ができます。ついでにやっちゃいましょう。
まとめ・修正版 Dockerfile
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
FROM base AS build
COPY . .
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run -r build
# 1. Root store に依存しない node_modules を作成する
RUN pnpm --filter app1 deploy --prod deploy/app1
RUN pnpm --filter app2 deploy --prod deploy/app2
# 本番
# 2. ベースイメージは base なので不要なファイルが含まれていない
FROM base AS app1
COPY /app/deploy/app1/node_modules/ /app/packages/app1/node_modules
COPY /app/deploy/app1/dist /app/packages/app1/dist
COPY /app/deploy/app1/package.json /app/packages/app1/package.json
WORKDIR /app/packages/app1
EXPOSE 8000
CMD [ "pnpm", "start" ]
FROM base AS app2
COPY /app/deploy/app2/node_modules/ /app/packages/app2/node_modules
COPY /app/deploy/app2/dist /app/packages/app2/dist
COPY /app/deploy/app2/package.json /app/packages/app2/package.json
WORKDIR /app/packages/app2
EXPOSE 8001
CMD [ "pnpm", "start" ]
ちゃんとリンク先がある状態でコピーされています。
$ docker build . --target app1 --tag app1:latest
$ docker run --rm -it app1:latest bash
---------
root@xxxxx:/app/packages/app1# ls -la node_modules/
total 12
drwxr-xr-x 3 root root 4096 Oct 16 10:10 .
drwxr-xr-x 1 root root 4096 Oct 16 10:11 ..
drwxr-xr-x 65 root root 4096 Oct 16 10:10 .pnpm
lrwxrwxrwx 1 root root 46 Oct 16 10:10 common -> .pnpm/file+packages+common/node_modules/common
lrwxrwxrwx 1 root root 43 Oct 16 10:10 date-fns -> .pnpm/date-fns@2.30.0/node_modules/date-fns
実行もできる。
$ docker run --rm -it app:latest
> app1@1.0.0 start /app/packages/app1
> node dist/main.js
common
app1
Discussion