id:pokutuna
Web Developer

Next.js を Google App Engine にデプロイする

GAE 便利さに気づいてからめちゃくちゃ気に入っている。

最近開発が活発な Cloud Run や、Firebase のバックエンド実行環境でもある Cloud Functions が話題に登りがちだけど、2nd gen になってからの GAE はめちゃめちゃいいです。

トラフィック分割などの機能も揃っているしハマりも少ないので Function レベルのコードを動かしたいときでも GAE を選びたくなることがある。GCP ドキュメントのサーバーレス オプションの選択フローチャートでも、おおよそ GAE に行き着くんじゃないかな。

最近個人ポートフォリオサイトを Next.js & GAE Standard Env で作ったので、その話を書く。

当初は単にこのブログに AdSense を貼りたかったんだけど、独自ドメインにしたので、まずルートドメインAdSense 審査を通す必要ができてしまった... なのでなんらかのコンテンツを pokutuna.com に置く必要に迫られたからなんだけども。

最小

package.json

{
  ...
  "scripts": {
    "start": "next start -p $PORT",
    ...
  }
}

app.yaml

runtime: nodejs12

handlers:
  - url: /.*
    script: auto
    secure: always
    redirect_http_response_code: 301

package.jsonnext を起動する際にポートを指定する、リクエストは全部アプリケーションで受ける。

GAE の Node ランタイムはアプリケーションを起動する際に start スクリプトを実行するので、期待する PORT 環境変数のポートで待ってあげる。大抵 8080 なのでハードコードされてる例をたまに見るけど、結構 8081 で来るよ。
Node.js ランタイム環境  |  Node.js 用 App Engine スタンダード環境に関するドキュメント

これぐらいで $ yarn build && gcloud app deploy で動く

_next/static 以下の配信

このままだと、静的ファイルも Next.js のアプリケーションから返しているので効率が悪い。_next/static 以下へのアクセスを、ビルドディレクト.next/static へ生成された静的ファイルへマッピングするようにする。

runtime: nodejs12

handlers:
  - url: /_next/static
    static_dir: .next/static

  - url: /.*
    script: auto
    secure: always
    redirect_http_response_code: 301

static_dir, static_files で GAE により配信される静的ファイルは、デフォルトで Cache-Control: public, max-age=600 で返る。どうせパスに BUILD_ID やハッシュ値が含まれるので、expiration: 365d などしてめちゃ長くキャッシュさせてもよいかも。
app.yaml 構成ファイル  |  Node.js 用 App Engine スタンダード環境に関するドキュメント

public 以下の配信

Next.js 9.1 から public/ 直下のファイルがそのままのパスで配信できるようになった。

単に public/ 以下に置くだけで参照できるのは良いけど、URL から pages/ 以下と区別するのはむつかしい。

以下のようにするのがおすすめ。

  • JSX から参照する画像などは諦めて /static/image.png のように参照して、/static 以下を public/staticマッピング
  • robots.txt などパスに制約があるファイルは static_files で個別にマッピング
handlers:
  - url: /_next/static
    static_dir: .next/static

  - url: /static
    static_dir: public/static

  - url: /robots.txt
    static_files: public/robots.txt
    upload: public/robots.txt

  - url: /.*
    script: auto
    secure: always
    redirect_http_response_code: 301

もし public/static のようなディレクトリを掘りたくないなら、静的ファイルっぽい拡張子を public/ 以下へマッピングする手もある。

handlers はリクエストごとに上から下へ評価されて最初にマッチするものに処理されるので、/_next/static より後に書けば Next.js にビルドされるアセットと拡張子がかぶっていても問題ないはず、もし拡張子に漏れがあってもアプリケーションから返るのでぶっ壊れはしないだろう。

handlers:
  - url: /_next/static
    static_dir: .next/static

  - url: /(.*\.(gif|png|jpg|ico|svg|css|txt))$
    static_files: public/\1
    upload: public/.*\.(gif|png|jpg|ico|svg|css|txt)$
  ...

アップロードするファイルを減らす

pages/ 以下やらなんやらはビルドに含まれるのでディレクトリをデプロイする必要はない。.gcloudignore で必要なものにしぼることでデプロイの高速化とイメージサイズの削減を見込める。

# ignore all
/*
 
# upload
!/app.yaml
!/package.json
!/package-lock.json
!/yarn.lock
!/public/
!/.next/

blog.pokutuna.com

しかし .next/ 以下はビルドごとに太っていく。ビルドディレクトリをきれいにする件についてはこういう issue があるみたいですね。 [RFC] cleaning distDir on next build · Discussion #6009 · vercel/next.js

ぼくはこういう Makefile で都度消してデプロイしてます。

PROJECT := YOUR_PROJECT_ID
GCLOUD := gcloud --project $(PROJECT)

.PHONY: build
build:
        yarn build

.PHONY: clean
clean:
        rm -rf .next/

.PHONY: deploy
deploy: clean build
        $(GCLOUD) app deploy

そんで作ったやつはこちら

pokutuna.com