14.

Next.js のデプロイ|next build とセルフホスト構成

編集

この記事の要点

  • next build してから next start で動かす Node.js サーバー方式が基本。全機能が使える。
  • Docker でまとめるなら output: "standalone"。実行に必要な最小構成だけが出力される。
  • NEXT_PUBLIC_ 付きの環境変数はビルド時にコードへ埋め込まれる。実行時に切り替えたい値には使えない。
  • 複数インスタンスで動かすなら、暗号鍵・デプロイ ID・共有キャッシュの 3 点セットを揃える。
  • リバースプロキシがレスポンスをためこむとストリーミングが機能しない。バッファリングを切る。

開発が終わったら本番環境に出します。Next.js は Node.js が動く環境なら基本的にどこでも動きますが、キャッシュや環境変数の扱いに独特の前提があり、そこを知らないと「ローカルでは動くのに本番でおかしい」という状態になります。この記事では自前サーバーで運用する場合を中心に扱います。

1デプロイ方式の選択

方式使える機能向いている場面
Node.js サーバーすべてVPS・専用サーバーでの通常運用
Docker コンテナすべてコンテナ基盤、環境の再現性を重視する場合
静的書き出し制限ありサーバーを持てない環境、純粋な静的サイト

静的書き出し(output: "export")は HTML と静的ファイルだけを出力する方式で、任意の Web サーバーに置けます。ただしサーバーを必要とする機能は使えません。proxy.ts のようにリクエストを見る仕組みは動かず、画像最適化も専用のローダーを別途用意する必要があります。

2Node.js サーバーとして動かす

npm run build
npm run start

これだけで全機能が使えます。ただし、Next.js のサーバーを直接インターネットに晒すのは推奨されていませんNginx などのリバースプロキシを前段に置き、不正なリクエストの遮断、接続数の制限、ペイロードサイズの上限といった役割を任せます。Next.js 側は描画に専念させる、という考え方です。

3Docker 向けの standalone 出力

コンテナに入れる場合、node_modules を丸ごと持ち込むとイメージが肥大します。output: "standalone" を指定すると、実行に必要なファイルだけがまとめて出力されます。

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'standalone',
}

export default nextConfig

複数のコンテナを立てる場合、同じビルド成果物を使い回すのが原則です。ステージごとにビルドし直すと、後述するビルド ID が食い違って問題が起きます。どうしてもビルドを分ける必要があるなら、generateBuildIdgit のコミットハッシュなどを固定値として与えます。

// next.config.js
module.exports = {
  generateBuildId: async () => {
    return process.env.GIT_HASH
  },
}

4環境変数はいつ読まれるか

ここが最も誤解されやすい部分です。環境変数には、ビルド時に固定されるものと、実行時に読まれるものがあります。

NEXT_PUBLIC_ 付き ブラウザからも読める next build の時点で値が埋め込まれる 実行時に環境変数を変えても効かない 秘密情報は絶対に入れない 接頭辞なし サーバーでのみ読める クライアント側では空文字になる connection() 後に読めば実行時の値 同じイメージを環境間で使い回せる
1 つのイメージを開発・検証・本番で使い回したいなら、環境ごとに変わる値は実行時に読む形にしておく。

実行時に確実に読みたい場合は、connection() を先に await します。これによりその処理がリクエスト時の実行に切り替わり、ビルド時の値が固定されるのを防げます。

import { connection } from 'next/server'

export default async function Page() {
  await connection()
  const value = process.env.RUNTIME_CONFIG   // 実行時に評価される
  return <p>{value}</p>
}

なお、バージョン 16 で serverRuntimeConfigpublicRuntimeConfig は削除されました。環境変数に置き換えてください。

5キャッシュの置き場所

生成済みページやキャッシュは、既定でインスタンスごとのメモリとローカルディスクに保存されます。1 台で動かし、ディスクが永続化されているなら、これで問題ありません。

問題になるのは複数台構成です。コンテナごとに別々のキャッシュを持つため、あるインスタンスで再検証しても他には伝わらず、古い内容が出続けます。この場合は共有ストアを使うカスタムのキャッシュハンドラを設定します。

// next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0,   // 既定のメモリキャッシュを無効化
}

ハンドラ側では get / set / revalidateTag を実装し、Redis などの外部ストアに保存します。加えて、タグの無効化を全インスタンスへ伝えるための refreshTags も実装しておくと、再検証の取りこぼしを防げます。

6複数インスタンスで必要な 3 点

項目設定するもの怠るとどうなるか
暗号鍵NEXT_SERVER_ACTIONS_ENCRYPTION_KEY「Server Action が見つからない」エラーが散発する
デプロイ IDdeploymentIdローリング更新中に古い資産を要求して失敗する
共有キャッシュcacheHandlerインスタンスごとに表示内容がずれる

暗号鍵はビルドごとに自動生成されるため、インスタンス間で食い違うと Server Function の復号に失敗します。base64 で 16・24・32 バイトのいずれかの鍵を用意し、ビルド時に環境変数で与えます。

デプロイ ID を設定すると、静的資産の URL にデプロイ識別子が付き、クライアントの持つ ID と食い違ったときは通常のページ遷移ではなく完全な再読み込みに切り替わります。新旧のバージョンが混在する時間帯を安全に乗り切るための仕組みです。

7ストリーミングを止めない

ストリーミングは、レスポンスを分割して送り続けることで成立します。前段のプロキシがバッファリングしていると、分割の意味が失われて最後にまとめて届きます。

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*{/}?',
        headers: [{ key: 'X-Accel-Buffering', value: 'no' }],
      },
    ]
  },
}

ロードバランサーやその手前の機器も、分割転送に対応している必要があります。1 か所でもためこむ設定があると、経路全体でストリーミングが無効になります。

8停止と再起動

after() で登録した後処理は、レスポンスを返した後に実行されます。プロセスを止めるときは SIGINTSIGTERM を送り、処理中のリクエストと保留中の後処理が終わるのを待ちます。停止までの猶予は 10〜30 秒程度を確保しておくのが目安です。いきなり強制終了すると、書き込み途中の処理が失われる可能性があります。

9つまずきやすいところ

  • 環境変数を変えたのに反映されないNEXT_PUBLIC_ 付きはビルド時に埋め込まれます。再ビルドが必要です。
  • CDN を挟んだら個人情報が他人に見えた:リクエスト依存の描画では Cache-Control: private が付きますが、CDN 側がこれを尊重する設定になっているか確認してください。
  • 画像最適化でメモリを食う:glibc 系の Linux では、メモリアロケータの設定が必要になる場合があります。
  • ビルドが Webpack 設定で失敗する:バージョン 16 では Turbopack が既定です。設定を移行するか --webpack を指定します。
  • デプロイ直後だけ遅い:キャッシュキーにビルド ID が含まれるため、新しいデプロイでは作り直しから始まります。仕様どおりの挙動です。

これでひととおりの流れが揃いました。最後に、既存プロジェクトをバージョン 16 へ持っていく際の注意点をまとめます。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点