2023.4.4
Docker + Next.jsにおける通常のMulti-Stage Buildとstandaloneのイメージサイズを比較する
概要
Next.jsには standaloneというオプション があります。
こちらを利用すると、本番環境で必要のないパッケージ(devDependencies)を含めないようにするのはもちろん、 next export
したときのようにサーバー機能を失うこともありません。
これらの恩恵を受けつつDockerのイメージサイズを削減できるとのことなので、どのくらいの効果があるのか検証してみます。
本来は一部のファイルをCDNで配信することで最適化できますが、今回の検証ではそのあたりのファイルもまとめてDockerイメージに含めた状態で比較してみます。
準備
今回はシンプルなNext.jsのアプリケーションを用意して、依存関係だけ調整したものをベースとして利用します。
まずはそちらの準備から行なっていきます。
bash_____terminal_____yarn create next-app
yarn remove @types/node @types/react @types/react-dom
yarn add -D @types/node @types/react @types/react-dom
package.json
はこれだけです。
json_____package.json_____{
"name": "multi-stage-build-test",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"eslint": "^8.37.0",
"eslint-config-next": "^13.2.4",
"next": "13.2.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.0.3"
},
"devDependencies": {
"@types/node": "18.15.11",
"@types/react": "18.0.33",
"@types/react-dom": "^18.0.11"
}
}
検証
それでは早速検証していきます。
node:16.13.2-alpine
今回は node:16.13.2-alpine
というDockerイメージを使って検証していくので、まずはなにもしない状態でのイメージサイズを確認します。
Dockerfile_____Dockerfile.plain_____FROM node:16.13.2-alpine
WORKDIR /workspace
CMD ["tail", "-f", "/dev/null"]
bash_____terminal_____docker build -t sample_plain -f Dockerfile.plain .
上記を実行すると 109MB
のDockerイメージができました。
脳死
続いて脳死状態でただただアプリケーションをビルドしてサーバーを起動するDockerイメージのサイズを確認します。
Dockerfile_____Dockerfile.normal_____FROM node:16.13.2-alpine
WORKDIR /workspace
COPY . .
RUN yarn --frozen-lockfile --production && yarn build
ENV NODE_ENV production
EXPOSE 3000
CMD ["yarn", "start"]
bash_____terminal_____docker build -t sample_normal -f Dockerfile.normal .
上記を実行すると 2.24GB
ものDockerイメージができてしまいました。
ただ、こちらは約1.7GBのyarnのキャッシュを含んでいる状態なので、次のMulti-Stage Buildではこちらも削除した状態のイメージを作成します。
Multi-Stage Build
続いてMulti-Stage BuildしたときのDockerイメージのサイズを確認します。
builderステージでビルドしたもののコピーと yarn start
するためにdependenciesだけインストールし、最後にyarnのキャッシュを削除します。
Dockerfile_____Dockerfile.production_____FROM node:16.13.2-alpine as builder
WORKDIR /workspace
COPY . .
RUN yarn --frozen-lockfile && yarn build
FROM node:16.13.2-alpine
WORKDIR /workspace
COPY --from=builder /workspace/.next ./.next
COPY --from=builder /workspace/public ./public
COPY package.json ./
RUN yarn --frozen-lockfile --production --ignore-scripts && yarn cache clean
ENV NODE_ENV production
EXPOSE 3000
CMD ["yarn", "start"]
bash_____terminal_____docker build -t sample_production -f Dockerfile.production .
# ...
[+] Building 132.9s (13/13) FINISHED
上記を実行すると 447MB
のDockerイメージができました。
yarnのキャッシュが約1.7GBと大きかったのでこれを削除するだけでもかなり小さくなります。
また、サーバー起動に不要なファイルをbuilderステージに置いてきているのでこのような結果となりました。
ちなみにビルド時間は 132.9s
でした。
standalone
最後に standalone
で確認してみます。
まず有効化するために next.config.js
に以下の1行を追加します。
javascript_____next.config.js_____/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone' // <- 追加
}
module.exports = nextConfig
そして以下のようにDockerfileを用意してビルドします。
Dockerfile_____Dockerfile.standalone_____# Must be standalone mode
FROM node:16.13.2-alpine as builder
WORKDIR /workspace
COPY . .
RUN yarn --frozen-lockfile && yarn build &&\
cp -r -u .next/server .next/standalone/.next &&\
cp -r -u .next/static .next/standalone/.next &&\
cp -r -u public .next/standalone
FROM node:16.13.2-alpine
WORKDIR /workspace
COPY --from=builder /workspace/.next/standalone ./
ENV NODE_ENV production
EXPOSE 3000
CMD ["node", "server.js"]
bash_____terminal_____docker build -t sample_standalone -f Dockerfile.standalone .
# ...
[+] Building 54.9s (10/10) FINISHED
上記を実行するとDockerイメージは 121MB
となりました!
ただのNext.jsアプリケーションとはいえほとんど node:16.13.2-alpine
の109MBと変わらないようなサイズとなりました。
もちろん上記のイメージからコンテナを作成してホストのポートとマッピングし localhost:3000
で確認すると問題なく閲覧できます。
ちなみにビルド時間は 54.9s
となっており、ビルド時間への明確な影響度はわかりかねますが今回の検証においては大幅なビルド時間短縮効果も得られました。
まとめ
今回はNext.jsのstandaloneオプションを利用しDockerのイメージサイズを確認してみました。
実際に開発していくアプリケーションでどこまでの効果が得られるかはプロジェクトごとに見ていきながらstandaloneオプションを利用していきたいと思います。