Next.jsのスタンドアロンモードでビルドしたイメージを Cloud Run へデプロイする
Next.js の experimental features のひとつに、スタンドアロンモードがあります。
通常モードでは、本番リリース可能なビルドを用意する場合、yarn build
による .next/
ディレクトリとあわせて node_modules
も含めます。依存関係を解決するために必要ですね。一方スタンドアロンモードを有効にした上で yarn build
するとビルド結果が異なります。.next/
ディレクトリが作られる点は同じですが、そこにstandalone
ディレクトリが追加されます。ここにはアプリを動かすためのファイルが依存関係も含めてすべて入っていて、.next/standalone/server.js
を起動すれば本番アプリが動かせます。ドキュメントによると、ビルドサイズを大幅に削減できるそうです。
この記事ではサンプルアプリに対してスタンドアロンモードの有無を変えDockerイメージをビルドし、イメージサイズの変化を確認してみます。その後、スタンドアロンモードでビルドしたDockerイメージを Cloud Run へデプロイし問題なく動くことを確認します。
サンプルアプリをスタンドアロンモードで
まず、 blog-starter-app
の雛形で Next.js アプリを作ります。
yarn create next-app --example blog-starter blog-starter-app
その後、いくつかのファイルに変更を加えます。
next.config.js
スタンドアロンモードを有効にします。
module.exports = {
images: {
domains: [
// "[yourapp].wpengine.com" (Update this to be your Wordpress application name in order to load images connected to your posts)
'secure.gravatar.com',
],
},
+ experimental: {
+ outputStandalone: true,
+ },
}
もともとの記述に追加します。これで、ビルド時に node_modules
をトレースしてくれます。
Dockerfile
ビルド用の Dockerfile をつくります。
FROM node:16 AS builder
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile --production=false
COPY . .
RUN yarn build
FROM node:16-stretch-slim AS runner
ENV NODE_ENV=production
WORKDIR /app
# standalone モードを利用すると、publicと.next/staticはデフォルトでは含まれないので明示的にコピーする必要があります
COPY /app/next.config.js ./
COPY /app/public ./public
COPY /app/.next/static ./.next/static
COPY /app/.next/standalone ./
CMD ["node", "server.js"]
yarn install
しなくてOK
本番用に まず、この Dockerfile はマルチステージビルドです。 FROM node:16 AS builder
をビルド用のイメージとし、--production=false
とすることで devDependencies も含めて yarn install
しています。その後いつもどおりにビルドしています。次に、FROM node:16-stretch-slim AS runner
は本番デプロイ用のイメージを作っています。ここに注目して欲しいのですが、yarn install
をしていません。スタンドアロンモードにより、必要なnode_modules
のパッケージはすべて.next/standalone
に集約されます。
必要なファイルを明示的にコピーする
スタンドアロンモードで作成された .next/standalone
さえあればサーバーが動作することは間違いないのですが、注意点があります。Next.jsのビルドプロセスは、public
や .next/static
フォルダを.next/standalone
へコピーしません。これらのフォルダは CDN によって処理されるのが理想的だからだそうです。
This minimal server does not copy the public or .next/static folders by default as these should ideally be handled by a CDN instead, although these folders can be copied to the standalone/public and standalone/.next/static folders manually, after which server.js file will serve these automatically.
Advanced Features: Output File Tracing | Next.js https://nextjs.org/docs/advanced-features/output-file-tracing
今回は静的ファイルもサーバーに含めてしまうため、Dockerfileの中でコピーしています。それが COPY --from=builder /app/public ./public
と COPY --from=builder /app/.next/static ./.next/static
です。/next.config.js
は、NextImageのドメイン設定など、起動時に参照する設定もあるようなので、含めておいていいと思います。
Cloud Run で動かす
これらのファイルを用意すれば、あとは Cloud Run へデプロイできます。こちらのブログでいろいろなやり方を紹介していますが…
今回はDockerfileを用意しておまかせコースでいきます。ローカルマシンからgcloud
コマンドが利用可能な状態としてください。こちらのブログに記載しています(Terraformは使いません)。
デプロイの前に、不要なファイルが入り込まないよう、.gcloudignore
を作っておきましょう。
# compiled output
/node_modules
# Logs
logs
*.log
# OS
.DS_Store
その後以下のデプロイコマンドを実行してください。
gcloud run deploy \
blog-starter-app \
--region asia-northeast2 \
--allow-unauthenticated \
--source .
Building using Dockerfile and deploying container to Cloud Run service [blog-starter-app]
✓ Building and deploying... Done.
✓ Uploading sources..
✓ Building Container...
✓ Creating Revision..
✓ Routing traffic..
✓ Setting IAM Policy..
Done.
Service URL: https://blog-starter-app-xxxx-dt.a.run.app
デプロイが成功するとURLが発行されます。スタンドアロンモードでサンプルアプリが問題なく動いていることを確認できました。
スタンドアロンモードを使わない場合と比較
.next.config.js
のスタンドアロンモード設定をOFFにして、Dockerfileも次のように少し違うものを使います。スタンドアロンモードを使わない、通常のビルド方法です。
FROM node:16 AS builder
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile --production=false
COPY . .
RUN yarn build
FROM node:16-stretch-slim AS runner
ENV NODE_ENV=production
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile --production=true
COPY /app/next.config.js ./
COPY /app/.next ./.next
COPY /app/public ./public
CMD ["yarn", "start"]
yarn build
で standalone
ディレクトリは生成されません。かわりに、node_modules
をインストールしています。こちらも Cloud Run へデプロイしてみましょう。
mv Dockerfile Dockerfile.standalone
mv Dockerfile.dekai Dockerfile
gcloud run deploy \
blog-starter-app \
--region asia-northeast2 \
--allow-unauthenticated \
--source .
こちらも無事デプロイ成功したようです。ビルドの経過情報は、Cloud Build(gcloudコマンドが内部的に起動しています)と Artifact Registry を見ればわかります。
ビルド時間を比較
ビルドにかかった時間を比較しましょう。Cloud Build をみます。
スタンドアロンモードなしの場合
まずはスタンドアロンモードを使用せず、node_modules
をインストールした場合です。
全体で約3分かかりました。余談ですが、gcloud run deploy
とすると、裏でワンショットの Cloud Build を起動しているようですね。docker コマンドを使ってビルドしている様子がわかります。
スタンドアロンモードを使った場合
スタンドアロンモードでのビルドとデプロイはすでに実行しているので、ひとつ前の結果と比較します。
お!1分47秒。1分ほど短縮しました。スタンドアロンモードではnode_modules
をインストールしないかわりに利用しているパッケージのトレースを行うことになるので、よくてトントンかなと思っていました。Next.jsの公式ページにもビルドサイズへの言及はありますが、ビルド時間への言及はないので、こちらはおまけくらいに思っていたほうが良さそうです。モノによっても変わりそうですね。
イメージサイズ
Artifact Registry でビルドしたイメージサイズが確認できます。裏で起動された Cloud Build は、成果物をこれまた勝手に Artifact Registry へ保存してくれるというわけです。
スタンドアロンモードなしの場合
379MBでした。う〜ん、このレベルのアプリにこのサイズはやっぱりちょっとサイズが大きい印象です。なんとなく。
スタンドアロンモードを使った場合
おお〜58.9MBです。大幅にスリムアップできました。この違いをみると、スタンドアロンモードを使うモチベーションになりますね。
Zenn では
ZennではフロントエンドのフレームワークとしてNext.jsを利用しています。先日のApp EngineからCloud Runへの移行のとき、実は一緒にスタンドアロンモードを導入しています。
最初はこのビルドサイズの減りっぷりに不安を覚えたものでした。チームからはこんな声が。
- やっぱり不安になるくらい小さくなりますね
- ほんとに動くの?これ?
- 怖いくらい普通に動いているのですが、ほんとにスタンドアロンモードが有効ですか?
E2Eテストや動作確認でも大きな問題は見つからなかったので、リリースに踏み切りました。現在のところ、問題なく動いていそうです。スタンドアロンモードを有効にしたおかげで、イメージサイズが小さくなり、docker build, docker push, docker pull の時間が削減できています。
おわりに:Next.jsコンテナで動かすなら
Next.jsは素晴らしいフレームワークであり、フロントエンドが抱えていたブラウザとサーバーの難しい統合を高いレベルで実現してくれると思っています。また、インフラ側は、コンテナとして詰め込んで動かせるプラットフォームも増えました。私たちが利用している Cloud Run もそのひとつです。コンテナのエコシステムはどんどん発展しているので、ぜひともその恩恵にあずかりたいですね。イメージサイズを小さく保てれば、CI/CDにかかる時間も減り、デプロイにかかるコストも少しずつ削減できます。長い目でみるとお得に使いたいですね。Next.jsコンテナをビルドしてデプロイしている方は、ぜひスタンドアロンモードを試してみてください。
Discussion