⏳
Nodejs(Nest.js)のアプリケーションのbuildを高速化、slim化してみようの会
前提
DockerによるNode.jsのインストール(pull)はキャッシュされているものとする
.dockerignoreは以下の通り
node_modules
.git
.gitignore
*.md
dist
test
最初にまとめ
- 軽く、そんなに依存関係が多くないアプリケーションであればnpmでstaging buildでキャッシュ効かせるぐらいでよいかも
-
RUN --mount=type=cache,target=
は効果がありそうである (https://zenn.dev/kou64yama/articles/powerful-docker-build-cache)を参考 - いろんなものを入れ始めた場合、開発環境とか、バージョン管理で色々入れ始めたら
pnpm fetch
は試してもいいかもしれない(https://pnpm.io/ja/cli/fetch)
- ただ、node_modulesの更新の場合は、少し時間がかかりやすいので注意する
npmをそのまま使ってビルド | npm installを先にする | npm+staging buildを使う | npm+staging build+各ステージでキャッシュを効かせる | node_modulesもキャッシュする | pnpmを使う | pnpm fetchを単純に使う | pnpm fetch+--mount=type=cache,target= とかもう少しやってみる |
|
---|---|---|---|---|---|---|---|---|
ビルドイメージの大きさ | 460MB | 460MB | 277.44MB | 277.44MB | 277.44MB | 279MB | 430MB | 279MB |
最初のビルド時間 | 10.4s | 10.8s | 11.1s | 10.3s | 13.6s | 11.1s | 20.2s | 17.8s |
ソースコード修正でのビルド | 10.4s | 3.6s | 9.5s | 5.6s | 4.8s | 4.3s | 5.4s | 4.2s |
package.jsonのversionに変更あり | 10.4s | 11.5s | 10.2s | 10.0s | 8.6s | 8.7s | 7.4s | 8.7s |
簡単な説明
RUN --mount=type=cache,target=
こいつがあると、RUN時に、特定のディレクトリをマウントしてくれる。
そして、そのディレクトリを各ビルドステージで共有できるというメリットが有る。
BuildKitが使える環境であることが前提。
corepack enable
corepack enable pnpm
などのように指定するのが普通のようである。バージョンは新し目のものを持ってくるが、corepack prepare pnpm@x.y.z --activate
のようにしてバージョンを固定するのもあり。
package.jsonのpackageManager
に指定すると (https://nodejs.org/api/packages.html#packagemanager) 同じように固定できる。
ただし、今回、package.jsonを先に使いたくないのでpackageManager
は使わず
pnpm fetch
こいつは package.json ではなくlockfileからインストールしてくれるというコマンド。
package.json自体の修正に対するキャッシュではなく、依存関係による修正にのみ反応するため、キャッシュが乗る確率が上がりやすくなる。
正直これで出来たものがnode_modulesに直接できるわけじゃないので注意する。
それぞれのパターンにおけるDockerfile
npmをそのまま使ってビルド
FROM node:20-slim
COPY . /app
WORKDIR /app
RUN npm i
RUN npm run build
EXPOSE 8000
CMD [ "npm", "run", "start:prod" ]
結果
- Image SIZE: 460MB
- 最初のビルド(10.4s)
- ソースコード修正でのビルド(10.4s)
- package.jsonのversionに変更ありのビルド(10.4s)
感想
- まぁ、知ってたけどサイズも大きいし、キャッシュされない
npm installを先にする
FROM node:20-slim
WORKDIR /app
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
RUN npm i
COPY . /app
RUN npm run build
EXPOSE 8000
CMD [ "npm", "run", "start:prod" ]
結果
- Image SIZE: 460MB
- 最初のビルド(10.8s)
- ソースコード修正のビルド(3.6s)
- package.jsonのversionに変更ありのビルド(11.5s)
感想
- そらそうよね。という結果。特に言うこともない
npm+staging buildを使う
FROM node:20-slim as base
WORKDIR /app
COPY . /app
FROM base as dev
RUN npm i && npm run build
FROM base as prod
RUN npm i --production
FROM node:20-slim
COPY /app/dist /app/dist
COPY /app/node_modules /app/node_modules
EXPOSE 8000
CMD [ "npm", "run", "start:prod" ]
結果
- Image SIZE: 277.44MB
- 最初のビルド(11.1s)
- ソースコード修正のビルド(9.5s)
- package.jsonのversionに変更ありのビルド(10.2s)
感想
- 何かしら並行に走るから若干早くなるのかなと思った(誤差だけど
- copyのときに差分が有るからキャッシュされていないという推測
npm+staging build+各ステージでキャッシュを効かせる
FROM node:20-slim as base
WORKDIR /app
FROM base as dev
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
RUN npm i
COPY . /app
RUN npm run build
FROM base as prod
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
RUN npm i --production
FROM node:20-slim
COPY /app/dist /app/dist
COPY /app/node_modules /app/node_modules
EXPOSE 8000
CMD [ "npm", "run", "start:prod" ]
結果
- Image SIZE: 277.44MB
- 最初のビルド(10.3s)
- ソースコード修正のビルド(5.6s)
- package.jsonのversionに変更ありのビルド(10.0s)
感想
- そもそもnpm installの際に結局キャッシュされていないから再実施しているため、あんまり速度は変わらん
node_modulesもキャッシュする
FROM node:20-slim as base
WORKDIR /app
FROM base as dev
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
RUN npm i
COPY . /app
RUN npm run build
FROM base as prod
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
RUN npm i --production
FROM node:20-slim
COPY /app/dist /app/dist
COPY /app/node_modules /app/node_modules
EXPOSE 8000
CMD [ "npm", "run", "start:prod" ]
結果
- Image SIZE: 277.44MB
- 最初のビルド(13.6s)
- ソースコードの修正のビルド(4.8s)
- package.jsonのversionに変更ありのビルド(8.6s)
感想
-
--mount=type=cache,target
を使うとキャッシュがきくのがみえてきた - これだとコード修正するとprod copyが走るのでcopyの順番はmoduleからのほうがいいかもですね
pnpmを使う
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY pnpm-lock.yaml /app/pnpm-lock.yaml
COPY package.json /app/package.json
WORKDIR /app
FROM base AS prod-deps
RUN pnpm install --prod --frozen-lockfile
FROM base AS build
RUN pnpm install --frozen-lockfile
COPY . /app
RUN pnpm run build
FROM base
COPY /app/node_modules /app/node_modules
COPY /app/dist /app/dist
EXPOSE 8000
CMD [ "pnpm", "start:prod" ]
結果
- Image SIZE: 279MB
- 最初のビルド(11.1s)
- ソースコードの修正のビルド(4.3s)
- package.jsonのversionに変更ありのビルド(8.7s)
感想
- 速度的には、pnpmだからなんだって感じですね。
- そんなにpackageも多くないから、多くなってきたときに単純に早いというところで意味があるかなぐらいですかね。
- この場合の弱点が、package.jsonを変化させたとき(versionだけの変更)に困るんですよね。
pnpm fetchを単純に使う
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
FROM base AS fetcher
COPY pnpm-lock.yaml /app/pnpm-lock.yaml
RUN pnpm fetch ./
FROM base AS prod-deps
COPY /pnpm/store /pnpm/store
COPY /app/node_modules /app/node_modules
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY package.json /app
RUN pnpm install --offline --prod --frozen-lockfile
FROM base AS build
COPY /pnpm/store /pnpm/store
COPY /app/node_modules /app/node_modules
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY package.json /app
RUN pnpm install --offline --frozen-lockfile
COPY . /app
RUN pnpm run build
FROM base
COPY /app/node_modules /app/node_modules
COPY /app/dist /app/dist
COPY package.json /app
EXPOSE 8000
CMD [ "pnpm", "start:prod" ]
結果
- Image SIZE: 430MB
- 最初のビルド(20.2s)
- ソースコードの修正のビルド(5.4s)
- package.jsonのversionに変更ありのビルド(7.4s)
感想
- SIZEがなんか全然減ってないですね。多分使い方を間違えているんでしょう。
- 最初のbuildがとても遅い。package.jsonから作ったほうが基本早いんだろうという想像。
pnpm fetch+`--mount=type=cache,target=`とかもう少しやってみる
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
FROM base AS fetcher
COPY pnpm-lock.yaml /app/pnpm-lock.yaml
RUN pnpm fetch ./
FROM base AS prod-deps
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY package.json /app
RUN pnpm install --offline --prod --frozen-lockfile
FROM base AS build
COPY /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY package.json /app
RUN pnpm install --offline --frozen-lockfile
COPY . /app
RUN pnpm run build
FROM base
COPY /app/node_modules /app/node_modules
COPY /app/dist /app/dist
COPY package.json /app
EXPOSE 8000
CMD [ "pnpm", "start:prod" ]
結果
- Image SIZE: 279MB
- 最初のビルド(17.8s)
- ソースコードの修正のビルド(4.2s)
- package.jsonのversionに変更ありのビルド(8.7s)
感想
- 前のやり方だと
/app/node_modules
がpnpm install —production
したところで、node_modulesが更新されなかったので、mountを利用するよう修正したらうまくいった - キャッシュしないように依存しているライブラリを増やした分ちょっと増えただけなので他のとおんなじぐらいslimになったっぽい
- 最終結果の数値としては変わらんが、インストールがofflineで入るので、たくさんパッケージが有るときにより有効っぽい?
- package.jsonがないとインストールが実行できないので、ここはキャッシュできない
- 最初のビルドはて、多分fetcherからのコピーではなくがmountによるコピーのため早くなってるんじゃないかと想像
Discussion