はじめに
Dockerイメージのサイズを見ちゃったりしてると玄人感出ますよね...
『目指せ、玄人!』ということで、NestJSでのマルチステージビルドについてまとめます📝
マルチステージビルドとは...
ざっくりいうと「ビルドするステージとランタイムで使用するステージを分けて記述する方式」です。
これにより、Dockerイメージを軽量化することができます!
公式
結論
私が考えたNestJS用のdockerfileはこちらです↓↓
FROM node:16.17.0 AS dist
WORKDIR /tmp
COPY ./app ./
RUN yarn install \
&& yarn build
FROM node:16.17.0 AS module
WORKDIR /tmp
COPY ./app ./
RUN yarn install --prod
FROM node:16.17.0-alpine
WORKDIR /usr/app
COPY --from=dist /tmp/dist ./dist
COPY --from=module /tmp/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/main"]
最終的に生成されたコンテナには、dist
とnode_modules
しか存在しません。
ここから順を追って解説
ディレクトリ構成
$ tree -L 2
.
├── README.md
├── app
│ ├── README.md
│ ├── nest-cli.json
│ ├── node_modules
│ ├── package.json
│ ├── src
│ ├── test
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── yarn.lock
└── dockerfile
nest new app
を実行して、dockerfile
を配置しただけの状態からスタートします💨
NestJSを立ち上げて、localhost:3000
にアクセスすると、Hello World!
と返ってきます。
dockerfile1
とりあえず動くコンテナ編
FROM node:16.17.0
WORKDIR /usr/app
COPY ./app ./
RUN yarn install
RUN yarn build
EXPOSE 3000
CMD ["yarn", "start:prod"]
めちゃくちゃよくありそうなdockerfile
。こちらでもちゃんとNestJSは動作します😺
イメージ確認1
$ docker build -t image:1 .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
image 1 6d8ba6caed1c 46 seconds ago 1.49GB
$ docker run --name nest -p3000:3000 --rm -d image:1
beb048664d49cf9997d85ead5844859d72e98dd73fc8f6b68fb6d9ed350ffe78
$ curl http://localhost:3000
Hello World!
※ 以後、curlでの確認は割愛します🙇♂️
一応動きますが、1.49GB
という激重コンテナが爆誕したわけです💣
ディレクトリ配下のすべての資源をDockerイメージに含めていることが主な原因です。コンテナの中を覗いてみると、ランタイムではいならい資源がたくさんあることがわかります。
$ docker run --rm image:1 ls
README.md
dist
nest-cli.json
node_modules
package.json
src
test
tsconfig.build.json
tsconfig.json
yarn.lock
ここから軽量化に向けて、少しずつ改善していこうと思います🤸♀️
dockerfile2
マルチステージビルド編
FROM node:16.17.0 AS build
WORKDIR /tmp
COPY ./app ./
RUN yarn install
RUN yarn build
FROM node:16.17.0
WORKDIR /usr/app
COPY --from=build /tmp/dist ./dist
COPY --from=build /tmp/package.json ./
COPY --from=build /tmp/node_modules ./node_modules
EXPOSE 3000
CMD ["yarn", "start:prod"]
出ました!マルチステージビルド🐳
NestJSのビルドのステージとランタイム実行用のステージを分離して記述しています。AS build
でステージに名前をつけることができ、--from=build
でそのステージを参照することができます!
イメージ確認2
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
image 2 ecaa05bb4a9a 2 minutes ago 1.2GB
お!!少しだけですが、軽くなりました!(1.49GB
→1.2GB
)
少ししか軽くなっていないと感じるかもしれませんが、アプリケーション開発が進むと、src
とtest
の容量がすぐに増えて、マルチステージビルドをしていないとどんどんDockerイメージが大きくなります。
dockerfile3
prod用のnode_modulesを作成して、package.jsonすらコンテナに含めないようにする編
FROM node:16.17.0 AS dist
WORKDIR /tmp
COPY ./app ./
RUN yarn install \
&& yarn build
FROM node:16.17.0 AS module
WORKDIR /tmp
COPY ./app ./
RUN yarn install --prod
FROM node:16.17.0
WORKDIR /usr/app
COPY --from=dist /tmp/dist ./dist
COPY --from=module /tmp/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/main"]
yarn install
は--prod
オプションをつけると、devDependencies
のモジュールをnode_modules
に含めないという挙動をとります。
ステージを2つに分けて、それぞれの成果物をランタイム用のコンテナに配置することで、より軽量なコンテナを作成することができます!
ついでに、package.json
のコマンドをCMD
に直接書くようにしました。そうすることで、ランタイムではpackage.json
がいらなくなります。
注意
以下に--prod
オプションを付ければいいと思うかもしれませんが、そうするとyarn build
でコケます...
NestJSのcliは、devDependencies
に存在しているためです。
RUN yarn install \
&& yarn build
イメージ確認1
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
image 3 a410579410f9 29 seconds ago 921MB
だいぶ軽くなりましたね!(1.2GB
→921MB
)
node_modules
のdevDependencies
がいかに重いかがよくわかります💥
dockerfile4(おまけ)
そもそも軽いnodeイメージを使うべき編
FROM node:16.17.0 AS dist
WORKDIR /tmp
COPY ./app ./
RUN yarn install \
&& yarn build
FROM node:16.17.0 AS module
WORKDIR /tmp
COPY ./app ./
RUN yarn install --prod
FROM node:16.17.0-alpine
WORKDIR /usr/app
COPY --from=dist /tmp/dist ./dist
COPY --from=module /tmp/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/main"]
node
イメージはdebian系の結構重めのイメージなので、ランタイムではもっと軽量なイメージを使いましょう!
用途に合わせて軽いイメージを選定してもらえるといいかなと思います!
REPOSITORY TAG IMAGE ID CREATED SIZE
node 16.17.0-alpine 5dcd1f6157bd 9 days ago 115MB
node 16.17.0-buster-slim af57506e8951 3 days ago 179MB
node 16.17.0-slim af57506e8951 3 days ago 179MB
node 16.17.0 c8af85aa3027 3 days ago 910MB
また、マルチステージビルドのイメージ(node
)は、軽量のものを使うかはおまかせします。最終的には破棄されるので、Dockerイメージのサイズには影響しません。ただ、イメージのpullに時間はかかるので、できる限り軽量のものを使うことをおすすめします。
動作確認4
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
image 4 91483413dff1 2 minutes ago 125MB
マルチステージビルドの前にこれやりなよって話ですね...(921MB
→125MB
)
bashすら入っていない超軽量イメージなので、コンテナの中に入って色々やりたい時はちょっと困るかもしれません。
まとめ
マルチステージビルドによって、Dockerイメージを軽量化することができました!
マルチステージビルドの導入によって、1.49GB
→921MB
になりました!
イメージが軽くなると、ECRへのpush、ECSのデプロイが早くなるので開発速度が上げられると思います!
この機会に皆さんのdockerfileを見直してみてください🙇♂️