はじめに
こんにちは、株式会社TOKOSのツキヤです!
Next.jsをVercel以外でデプロイする際、コンテナ系のサービス(ex. ECS)を用いることがぼちぼちあるかと思います。
その際に、どうやってイメージサイズを圧縮するかは少し考えたりしますよね?
今回「マルチステージビルド」「standalone(スタンドアロン)モード」の2つの方法を用いてNext.jsのイメージサイズを圧縮する方法を記述します💪
なるべく分かりやすく、最終的にはコピペで動くように説明しますので、ぜひ最後まで読んでください!
対象読者・前提
- Next.jsの基礎的な知識がある人
- Docker・docker-composeの基礎的な知識がある人
マルチステージビルドとは?
マルチステージビルドに関してはご存知の方も多いかとは思いますが、簡単に説明だけします!
結論から言うと「最終的なビルドイメージのサイズを減らすための仕組み」です。
、、、これだけだと分からないと思うので、もう少し解説します😇
通常、アプリケーションを作る過程では、コンパイルや依存パッケージ・ライブラリのインストールなど、ビルドに必要なファイルがたくさん作られます。しかし、実際に動かす際にはこれらのファイルは不要ですよね。
例えば、昔のDockerfile
だとhoge
というミドルウェアをインストールした後、そのhoge
に対するtmp
ファイルやcache
ファイルをrm
で削除する記述が見られると思います。
# ...
RUN apt-get update -qq \
&& apt-get install -y --no-install-recommends \
curl \
git \
libmariadb-dev \
build-essential \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
# ...
ですが、マルチステージビルドを使うことでそういった記述を無くし、アプリケーションの実行に本当に必要なパッケージ・ファイルのみでビルドをすることができます!
具体例は下記します😎
standalone(スタンドアロン)モードとは?
こちらについてはNext.js固有の設定方法なので、もしかしたら知らない方も居るかもしれないです。
standaloneモードについてひとことで言うと「本番デプロイに必要なファイルのみをコピーしたstandalone
フォルダを自動的に作成する」機能です!
つまり、Dockerイメージ内にこのstandalone/
配下のディレクトリのみを入れることでイメージサイズをかなり圧縮できそうな気がしますね✨
こちらも具体例は今から説明していきます!
詳細が気になる方は下記公式サイトをご確認ください。
マルチステージビルドを使ったイメージの改善
早速、まずはマルチステージビルドを使うとどの程度イメージサイズが変わるかを確認してみます!
そのために、Next.jsのプロジェクトを create-next-app で作成し、イメージを作ってみます!
$ npx create-next-app@latest docker-project --ts --eslint
上記コマンドでdocker-project
というプロジェクトを作成します!
tailwindは入れておきます!
できあがったプロジェクトに対して、Dockerfile
とdocker-compose.yml
を作成して起動してみます!
services:
app:
build:
context: .
dockerfile: Dockerfile
image: docker-project
ports:
- 3000:3000
docker-compose.yml
についてはただの起動用です!
そして、比較用となるDockerfile
を上記にしてみます!
(今回はNode.jsのバージョンは20系にしています)
FROM node:20.13-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "run", "start"]
この時点では、マルチステージビルドもstandaloneモードも使用していないです。$ docker compose up
で立ち上がればOKです。
この時、イメージサイズは1.35GBでした。結構大きめですね。。
ここから、マルチステージビルド化を行います!
早速、マルチステージビルド化したDockerfileを下記します。
FROM node:20.13-slim AS base
# ビルドステージ
FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
# 実行ステージ
FROM base AS runner
WORKDIR /app
COPY --from=builder /app/package.json /app/package-lock.json ./
RUN npm install
COPY --from=builder /app/.next ./.next
# public/配下に画像等を配置する場合はコメントアウトを外す
# COPY --from=builder /app/public ./public
CMD ["npm", "run", "start"]
結論から言うと、上記記述に変更することでイメージサイズが812MBになりました✨
比較用と比べると4割位削減できていますね!
それでは簡単に説明します。
まずは1行目部分です。
FROM node:20.13-slim AS base
これで、node:20.13-slim
というベースイメージにbase
というエイリアスをつけました。
これにより、もしバージョンを変更したい場合は1行目のみを変更するだけでよくなります。
次にビルドフェーズです。
# ビルドステージ
FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
ここでは、単純にnpm install
をしてからnpm run build
をしているだけです。
このbuilder
と名付けたイメージは、最終成果物には含まれないのが特徴です!
最後に実行フェーズです。
# 実行ステージ
FROM base AS runner
WORKDIR /app
COPY --from=builder /app/package.json /app/package-lock.json ./
RUN npm install
COPY --from=builder /app/.next ./.next
# public/配下に画像等を配置する場合はコメントアウトを外す
# COPY --from=builder /app/public ./public
CMD ["npm", "run", "start"]
この部分での記載が実際のイメージに含まれるようになります。
特徴的なのは、--from=builder
の部分ですね!
この記述がある部分でのCOPY
は、前段のbuilder
内のディレクトリを参照することになります。
つまり、COPY --from=builder /app/.next ./.next
は「builder
イメージ内の/app/.next
ディレクトリをrunner
イメージ内の./.next
にコピーして」という命令になります!
何が嬉しいかというと、runner
イメージではnpm run build
をせずとも必要なファイル群をコピーすることでnpm run start
を実行することができるということです✨
これで、npm run build
時に生成される、実行時には不要なファイル群を取り除くことができ、結果としてイメージサイズの削減になっているのです!
さて、この時点でもまあまあイメージサイズを削減できているのですが、ここから更に削減していきます🔥🔥
standalone(スタンドアロン)モードを使ったイメージの改善
ここからstandaloneモードに仕上げていくのですが、1点だけnext.config.(m)js
の修正が必要です!
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
export default nextConfig;
上記のようにoutput: "standalone"
とするだけで準備OKです!
こちらも、早速ですがDockerfile全体を記載します!
FROM node:20.13-slim AS base
# ビルドステージ
FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
# 実行ステージ
FROM base AS runner
WORKDIR /app
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/standalone ./
# public/配下に画像等を配置する場合はコメントアウトを外す
# COPY --from=builder /app/public ./public
ENV HOSTNAME "0.0.0.0"
ENV PORT 3000
EXPOSE 3000
CMD ["node", "server.js"]
変更点は実行フェーズ以下です。
まず、COPYの対象が変わりました!
必須となるのが.next/static
と.next/standalone
です。static
の方はそのまま同じパスにコピーするのですが、standalone
の方はカレントディレクトリ直下にコピーします!
次に、ホスト名とポートを設定します。
環境変数名HOSTNAME
とPORT
にそれぞれ設定したい値を入れるだけでOKです💪
最後に、サーバー起動コマンドの実行です。
ここは$ node server.js
というコマンドを実行してあげましょう!
以上で localhost:3000 で問題無くアクセスできるはずです。
そして、作成されたイメージサイズはなんと239MBです!!
マルチステージビルドのみと比べても7割以上削減できています✨
おわりに
今回は、Next.jsに対してマルチステージビルドとstandaloneモードを使ってDocker
のイメージサイズを削減する方法を説明しました!
イメージサイズを削減することで、CI/CDの時間も短縮されたりして良いことしか無いはずなので積極的に使っていきたいですね💪
逆に、こうしたらもっと削減できる等の方法を知っている方が居ましたらご教示いただきたいです🙏
尚、今回のDocerfile
はバインドマウント等を行っていないので、開発時にはバインドマウントをdocker-dompose.yml
内に記述して別のDockerfile
を用意した方が良いかもしれないです。