Docker Multi Stage BuildとNext.jsのstandaloneでDockerイメージ容量を75%削減した話
はじめに
株式会社 Rehab for JAPAN のまっちゃんです。
今回は、Next.jsアプリケーションのDockerイメージ容量を削減した話を共有します。初回リリース以降、ほぼ手を加えていなかったDockerfileをふと見直してみたところ、イメージ容量が予想以上に大きいことに気付きました。特に、容量が大きいことによる問題として以下が挙げられます。
ECR(Amazon Elastic Container Registry)のコスト増加
CI/CDのビルド時間の増加
これらを解決するために、Dockerfileを見直し、最適化を行いました。
結果、Dockerイメージ容量を約75%削減することに成功しました。
結果
削減容量
Before | After | 差分 |
---|---|---|
978MB | 260MB | 718MB |
(検証環境: Next.js 14.2.18、React 18)
得られたメリット
-
コスト削減
ECRの容量と転送量に基づく料金が削減されました。 -
ビルド速度向上
CI/CDでのDockerイメージのアップロード時間が短縮され、全体的なパイプラインの実行時間が改善されました。
作業内容
今回は、Dockerの マルチステージビルドと、Next.jsのstandaloneを使用して、Dockerfileの容量を節約しました。
マルチステージビルドとは
Docker のマルチステージビルドとは、複数のFROM
ステートメントを使用しイメージをビルドする方法です。一つのFROM
が一つのステージに当たります。ステージごとにベースイメージを変更でき、前のステージからファイルをコピーすることが可能です。最後のステージが最終的なイメージになり、他のステージは明示的にコピーしない限り最終イメージには含まれません。その結果、最終イメージにビルド時のみ必要なファイルを除外することができ、イメージの容量を減らすことができます。
Docker17.05 以上で使用可能になった機能です。
修正前の Dockerfile
FROM node:22.11.0-slim
ENV APP_ROOT=/var/www/app
WORKDIR $APP_ROOT
ADD . $APP_ROOT
RUN npm install --omit=dev
RUN npm build
EXPOSE 3000
CMD [ "npm", "start" ]
イメージ図
本 Docker ファイルでは、node:22.11.0-slim
をベースイメージとして使用し、RUN npm install --omit=dev
、npm run build
を実行しています。しかし、ADD . $APP_ROOT
で全量コピーしているため、不要なファイルも含まれてしまっています。結果的に、イメージの容量が 1GB近くになっていました。
修正後の Dockerfile
FROM node:22.11.0-slim AS base
ENV APP_ROOT=/var/www/app
WORKDIR $APP_ROOT
ADD . $APP_ROOT
RUN npm install --omit=dev
RUN npm build
FROM base AS release
WORKDIR $APP_ROOT
COPY /var/www/app/package.json /var/www/app/package-lock.json ./
COPY /var/www/app/node_modules ./node_modules
COPY /var/www/app/.next ./.next
EXPOSE 3000
CMD [ "npm", "start" ]
イメージ図
977MB から 780MB までイメージの容量を削減することができました。
Next.jsのstandaloneを使用する
Next.jsでは、build時に運用環境に必要なファイルのみをビルド結果に含めるstandalone
オプションがあります。このオプションを使用することで、ビルド結果のディレクトリ./next
にstandalone
ディレクトリが追加され、内部にnode_modules
を含めることができます。また、next start
の代わりにとなる最小限のエントリーポイントファイル(server.js
)が出力されます。このserver.js
ファイルを起動することで、Next.jsアプリケーションを起動することができます。
このstandalone
オプションを使用した際には、public
ディレクトリ、.next/static
は含まれなくなるため、必要であれば別途コピーする必要があります。Next.jsの考え方としては、CDNによる静的ファイルの配信を推奨しているため、public
ディレクトリは含まれないようになっているようです。
standaloneのオプションを有効にする方法は、next.config.js
に以下のように記述します。
module.exports = {
output: 'standalone',
}
standalone版のDockerfile
FROM node:22.11.0-slim AS base
WORKDIR /var/www/app
COPY package*.json ./
RUN npm install --omit=dev
COPY . ./
RUN npm run build
FROM node:22.11.0-slim AS release
WORKDIR /var/www/app
COPY /var/www/app/.next/standalone ./
COPY /var/www/app/.next/static ./.next/static
COPY /var/www/app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
この結果、780MB から 260MB までイメージの容量を削減することができました。
まとめ
今回の改善では以下の結果が得られました。
- Dockerイメージ容量の削減:978MB → 260MB
- CI/CDのビルド時間の短縮:運用中のプロジェクトでは、Dockerイメージのアップロード時間が短縮されたことで、パイプライン全体の実行時間を約2分削減することに成功しました。
- ECRのコスト削減
マルチステージビルドとNext.jsのstandaloneオプションを組み合わせることで、Next.jsアプリケーションのDockerイメージを効率的に最適化することができました。
Discussion