Next.jsアプリケーションのセルフホスティング方法

Next.jsアプリをデプロイする際、インフラに基づいてさまざまな機能の扱いを設定したい場合があります。

🎥 動画で学ぶ: Next.jsのセルフホスティングについて詳しく知る → YouTube (45分).

画像最適化

next/imageによる画像最適化は、next startを使用してデプロイする場合、設定なしでセルフホスティング環境で動作します。別のサービスで画像を最適化したい場合は、画像ローダーを設定できます。

静的エクスポートでも、next.config.jsでカスタム画像ローダーを定義することで画像最適化を使用できます。画像はビルド時ではなく実行時に最適化されることに注意してください。

知っておくと良いこと:

  • glibcベースのLinuxシステムでは、画像最適化に追加設定が必要で、メモリ使用量が過剰になるのを防げます。
  • 最適化された画像のキャッシュ動作とTTLの設定方法について詳しく学べます。
  • 画像最適化を無効にしつつnext/imageの他の利点を保持することも可能です。例えば、別途画像を自分で最適化している場合などです。

ミドルウェア

ミドルウェアは、next startを使用してデプロイする場合、設定なしでセルフホスティング環境で動作します。着信リクエストへのアクセスが必要なため、静的エクスポートではサポートされていません。

ミドルウェアはEdgeランタイムを使用し、アプリケーションのすべてのルートやアセットの前で実行される可能性があるため、低遅延を確保するために利用可能なNode.js APIのサブセットを提供します。これが必要ない場合は、完全なNode.jsランタイムを使用してミドルウェアを実行できます。

すべてのNode.js APIを必要とするロジック(または外部パッケージ)を追加したい場合、このロジックをレイアウト内のサーバーコンポーネントに移動できる可能性があります。例えば、ヘッダーの確認やリダイレクトなどです。また、next.config.jsでヘッダー、クッキー、クエリパラメータを使用してリダイレクト書き換えも可能です。それでもうまくいかない場合は、カスタムサーバーも使用できます。

環境変数

Next.jsはビルド時と実行時の両方の環境変数をサポートしています。

デフォルトでは、環境変数はサーバーでのみ利用可能です。ブラウザに環境変数を公開するには、NEXT_PUBLIC_をプレフィックスとして付ける必要があります。ただし、これらの公開環境変数はnext build時にJavaScriptバンドルにインライン化されます。

実行時環境変数を読み取るには、getServerSidePropsを使用するか、App Routerを段階的に採用することを推奨します。

これにより、異なる値を持つ複数の環境間でプロモート可能な単一のDockerイメージを使用できます。

知っておくと良いこと:

キャッシュとISR

Next.jsはレスポンス、生成された静的ページ、ビルド出力、および画像、フォント、スクリプトなどの静的アセットをキャッシュできます。

キャッシュとページの再検証(Incremental Static Regeneration(ISR)を使用)は同じ共有キャッシュを使用します。デフォルトでは、このキャッシュはNext.jsサーバーのファイルシステム(ディスク上)に保存されます。これはPages RouterとApp Routerの両方を使用してセルフホスティングする場合に自動的に動作します

キャッシュされたページとデータを耐久性のあるストレージに永続化したり、Next.jsアプリケーションの複数のコンテナやインスタンス間でキャッシュを共有したりしたい場合は、Next.jsキャッシュの場所を設定できます。

自動キャッシュ

  • Next.jsは真に不変なアセットに対してpublic, max-age=31536000, immutableCache-Controlヘッダーを設定します。これは上書きできません。これらの不変ファイルにはファイル名にSHAハッシュが含まれているため、無期限に安全にキャッシュできます。例えば、静的画像インポートなどです。画像のTTLを設定できます。
  • Incremental Static Regeneration(ISR)はs-maxage: <getStaticPropsでの再検証時間>, stale-while-revalidateCache-Controlヘッダーを設定します。この再検証時間はgetStaticProps関数で秒単位で定義されます。revalidate: falseを設定すると、デフォルトで1年間のキャッシュ期間が適用されます。
  • 動的にレンダリングされたページは、ユーザー固有のデータがキャッシュされないようにprivate, no-cache, no-store, max-age=0, must-revalidateCache-Controlヘッダーを設定します。これはApp RouterとPages Routerの両方に適用されます。Draft Modeも含まれます。

静的アセット

静的アセットを別のドメインやCDNでホストしたい場合は、next.config.jsassetPrefix設定を使用できます。Next.jsはJavaScriptやCSSファイルを取得する際にこのアセットプレフィックスを使用します。アセットを別のドメインに分離すると、DNSとTLS解決に余分な時間がかかるという欠点があります。

assetPrefixについて詳しく学ぶ

キャッシュの設定

デフォルトでは、生成されたキャッシュアセットはメモリ(デフォルトで50MB)とディスクに保存されます。Kubernetesのようなコンテナオーケストレーションプラットフォームを使用してNext.jsをホストしている場合、各ポッドはキャッシュのコピーを持ちます。ポッド間でキャッシュが共有されないため、デフォルトでは古いデータが表示されるのを防ぐために、Next.jsキャッシュを設定してカスタムキャッシュハンドラーを提供し、メモリ内キャッシュを無効にできます。

セルフホスティング時にISR/データキャッシュの場所を設定するには、next.config.jsファイルでカスタムハンドラーを設定します:

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

次に、プロジェクトのルートにcache-handler.jsを作成します。例:

cache-handler.js
const cache = new Map()

module.exports = class CacheHandler {
  constructor(options) {
    this.options = options
  }

  async get(key) {
    // これは耐久性のあるストレージなど、どこにでも保存できます
    return cache.get(key)
  }

  async set(key, data, ctx) {
    // これは耐久性のあるストレージなど、どこにでも保存できます
    cache.set(key, {
      value: data,
      lastModified: Date.now(),
      tags: ctx.tags,
    })
  }

  async revalidateTag(tags) {
    // tagsは文字列または文字列の配列です
    tags = [tags].flat()
    // キャッシュ内のすべてのエントリを反復処理
    for (let [key, value] of cache) {
      // 値のタグに指定されたタグが含まれている場合、このエントリを削除
      if (value.tags.some((tag) => tags.includes(tag))) {
        cache.delete(key)
      }
    }
  }

  // 単一リクエスト用の一時的なメモリ内キャッシュが必要で、
  // 次のリクエスト前にリセットしたい場合はこのメソッドを活用できます
  resetRequestCache() {}
}

カスタムキャッシュハンドラーを使用すると、Next.jsアプリケーションをホストするすべてのポッド間で一貫性を確保できます。例えば、キャッシュされた値をRedisやAWS S3など、どこにでも保存できます。

知っておくと良いこと:

  • revalidatePathはキャッシュタグの上にある便利なレイヤーです。revalidatePathを呼び出すと、指定されたページの特別なデフォルトタグでrevalidateTag関数が呼び出されます。

ビルドキャッシュ

Next.jsはnext build中にアプリケーションのバージョンを識別するIDを生成します。同じビルドを複数のコンテナで使用して起動する必要があります。

環境の各ステージでリビルドする場合、コンテナ間で使用する一貫したビルドIDを生成する必要があります。next.config.jsgenerateBuildIdコマンドを使用します:

next.config.js
module.exports = {
  generateBuildId: async () => {
    // これは何でも構いません、最新のgitハッシュを使用する例
    return process.env.GIT_HASH
  },
}

バージョンスキュー

Next.jsはバージョンスキューのほとんどのインスタンスを自動的に軽減し、検出時に新しいアセットを取得するためにアプリケーションを自動的にリロードします。例えば、deploymentIdに不一致がある場合、ページ間の遷移はプリフェッチされた値を使用するのではなく、ハードナビゲーションを実行します。

アプリケーションがリロードされると、ページナビゲーション間で状態が保持されないように設計されている場合、アプリケーション状態が失われる可能性があります。例えば、URL状態やローカルストレージを使用すると、ページリフレッシュ後も状態が保持されます。しかし、useStateのようなコンポーネント状態はそのようなナビゲーションで失われます。

手動の適切なシャットダウン

セルフホスティング時、SIGTERMまたはSIGINTシグナルでサーバーがシャットダウンする際にコードを実行したい場合があります。

環境変数NEXT_MANUAL_SIG_HANDLEtrueに設定し、_document.jsファイル内でそのシグナルのハンドラーを登録できます。環境変数は.envファイルではなく、package.jsonスクリプトに直接登録する必要があります。

知っておくと良いこと: 手動シグナル処理はnext devでは利用できません。

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "NEXT_MANUAL_SIG_HANDLE=true next start"
  }
}
pages/_document.js
if (process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGTERM', () => {
    console.log('SIGTERMを受信: クリーンアップ中')
    process.exit(0)
  })
  process.on('SIGINT', () => {
    console.log('SIGINTを受信: クリーンアップ中')
    process.exit(0)
  })
}

On this page