Node.jsのDockerイメージを使ってマルチステージビルドをやってみた

Node.jsのサーバーアプリケーションをDockerで動かす上で、マルチステージビルドをやってみたくなったので試してみました。

マルチステージビルド

Dockerのイメージ作成時に行うパッケージのインストールやコンテナ内でのビルドといったタスクを階層毎に行う事と認識しています。
結果的に1階層で行うときよりもDockerイメージのサイズを抑えて、実行環境でのパフォーマンスを向上させたり、最終的なイメージにランタイムなどの必要なツールのみ追加してセキュリティリスクを抑えることができる良さもあるようです。

マルチステージビルドの利用 - Docker docs

やってみる

Dockerfile.singleとDockerfile.multiを作って、それぞれビルドしてみます。
src/app.tsという簡単なスクリプトを置き、dist/app.jsにビルドしたものを出力するようなnpm scriptsを定義しています。ディレクトリ構成としては以下のようになります。

├── Dockerfile.single
├── Dockerfile.multi
├── node_modules
├── package-lock.json
├── package.json
├── dist
│   └── app.js
└── src
    └── app.ts

マルチステージビルドをしなかった場合

Dockerfile.singleは以下のような内容にしました。

FROM node:18-slim

WORKDIR /usr/src/app

COPY . .
RUN npm ci

RUN npm run build

EXPOSE 8080
CMD [ "node", "dist/src/app.js" ]
$ sudo docker build -t single -f Dockerfile.single

このようなイメージサイズになりました。

マルチステージビルドしなかった場合のイメージサイズ

マルチステージビルドをした場合

Dockerfile.multiは以下のような内容にしました。

FROM node:18-slim as deps
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci

FROM node:18-slim as builder
WORKDIR /usr/src/app
COPY . .
COPY --from=deps /usr/src/app/node_modules ./node_modules
RUN npm run build

FROM node:18-slim as runner
USER node
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=builder /usr/src/app/node_modules ./node_modules

EXPOSE 8080
CMD [ "node", "dist/src/app.js" ]

$ sudo docker build -t multi -f Dockerfile.multi

このようなイメージサイズになりました。

マルチステージビルドした場合のイメージサイズ

少し分かりづらいですが、マルチステージビルドの方が10MBほど小さくなりました

今回はパッケージもほとんど入っていなかったためnode_modulesをまるっと移行しましたが、ランタイム時に必要なパッケージのみインストールで問題ない状況になるとだいぶ変わりそうな気がします。

感想

まだ調整できる箇所はあるかと思いますが、これだけでも少しサイズを落とせることが分かったのは良かったです。

また、Goなんかだとaplineみたいな小さいイメージでコンパイル済みのものを動かせるようなのでより最適化できそうな気がします。
こういった技術もタイミング見て少しずつやっていければと思います。