🐳

Docker Multi Stage BuildとNext.jsのstandaloneでDockerイメージ容量を75%削減した話

2024/11/19に公開

はじめに

株式会社 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

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 run build

EXPOSE 3000
CMD [ "npm", "start" ]

イメージ図

本 Docker ファイルでは、node:22.11.0-slimをベースイメージとして使用し、RUN npm install --omit=devnpm run buildを実行しています。しかし、ADD . $APP_ROOTで全量コピーしているため、不要なファイルも含まれてしまっています。結果的に、イメージの容量が 1GB近くになっていました。

修正後の Dockerfile

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 run build

FROM base AS release

WORKDIR $APP_ROOT

COPY --from=base /var/www/app/package.json /var/www/app/package-lock.json ./
COPY --from=base /var/www/app/node_modules ./node_modules
COPY --from=base /var/www/app/.next ./.next

EXPOSE 3000
CMD [ "npm", "start" ]

イメージ図

977MB から 780MB までイメージの容量を削減することができました。

Next.jsのstandaloneを使用する

Next.jsでは、build時に運用環境に必要なファイルのみをビルド結果に含めるstandaloneオプションがあります。このオプションを使用することで、ビルド結果のディレクトリ./nextstandaloneディレクトリが追加され、内部にnode_modulesを含めることができます。また、next startの代わりにとなる最小限のエントリーポイントファイル(server.js)が出力されます。このserver.jsファイルを起動することで、Next.jsアプリケーションを起動することができます。
このstandaloneオプションを使用した際には、publicディレクトリ、.next/staticは含まれなくなるため、必要であれば別途コピーする必要があります。Next.jsの考え方としては、CDNによる静的ファイルの配信を推奨しているため、publicディレクトリは含まれないようになっているようです。

standaloneのオプションを有効にする方法は、next.config.jsに以下のように記述します。

next.config.js
module.exports = {
  output: 'standalone',
}

standalone版のDockerfile

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 --from=base /var/www/app/.next/standalone ./
COPY --from=base /var/www/app/.next/static ./.next/static
COPY --from=base /var/www/app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]

この結果、780MB から 260MB までイメージの容量を削減することができました。

まとめ

今回の改善では以下の結果が得られました。

  1. Dockerイメージ容量の削減:978MB → 260MB
  2. CI/CDのビルド時間の短縮:運用中のプロジェクトでは、Dockerイメージのアップロード時間が短縮されたことで、パイプライン全体の実行時間を約2分削減することに成功しました。
  3. ECRのコスト削減
    マルチステージビルドとNext.jsのstandaloneオプションを組み合わせることで、Next.jsアプリケーションのDockerイメージを効率的に最適化することができました。
Rehab Tech Blog

Discussion