📌

Next.jsのスタンドアロンモードでビルドしたイメージを Cloud Run へデプロイする

waddy_u2022/06/12に公開

Next.js の experimental features のひとつに、スタンドアロンモードがあります。

https://nextjs.org/docs/advanced-features/output-file-tracing

通常モードでは、本番リリース可能なビルドを用意する場合、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

スタンドアロンモードを有効にします。

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 をつくります。

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 --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static

COPY --from=builder /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 ./publicCOPY --from=builder /app/.next/static ./.next/static です。/next.config.jsは、NextImageのドメイン設定など、起動時に参照する設定もあるようなので、含めておいていいと思います。

Cloud Run で動かす

これらのファイルを用意すれば、あとは Cloud Run へデプロイできます。こちらのブログでいろいろなやり方を紹介していますが…

https://zenn.dev/waddy/articles/nestjs-cloudrun-deploy

今回はDockerfileを用意しておまかせコースでいきます。ローカルマシンからgcloudコマンドが利用可能な状態としてください。こちらのブログに記載しています(Terraformは使いません)。

デプロイの前に、不要なファイルが入り込まないよう、.gcloudignoreを作っておきましょう。

.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も次のように少し違うものを使います。スタンドアロンモードを使わない、通常のビルド方法です。

Dockerfile.dekai
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 --from=builder /app/next.config.js ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
CMD ["yarn", "start"]

yarn buildstandaloneディレクトリは生成されません。かわりに、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コンテナをビルドしてデプロイしている方は、ぜひスタンドアロンモードを試してみてください。

Zenn Tech Blog

Zenn開発チームのテックブログです。Zennの開発・運用にまつわる技術的な知見を投稿します。主な技術スタックは React / Next.js / TypeScript / Ruby on Rails / Google Cloud などです。

Discussion

ログインするとコメントできます