Menu

Next.jsアプリケーションをセルフホストする方法

Next.jsアプリをデプロイする際、インフラストラクチャに基づいて異なる機能の取り扱い方を設定したい場合があります。

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

画像最適化

next/imageを通じた画像最適化は、next startを使用してデプロイする際に追加設定なしでセルフホストで動作します。画像を最適化するための別のサービスを使用したい場合は、画像ローダーを設定できます。

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

補足:

  • glibc搭載のLinuxシステムでは、過剰なメモリ使用を防ぐために画像最適化に追加の設定が必要な場合があります。
  • 最適化された画像のキャッシュ動作とTTLの設定方法について詳しく学ぶことができます。
  • 必要に応じて画像最適化を無効化しても、next/imageを使用する他のメリットは維持できます。例えば、画像を別途自分で最適化している場合などです。

ミドルウェア

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

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

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

環境変数

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

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

ランタイム環境変数を読み取るには、getServerSidePropsを使用するか、Appルーターを段階的に採用することをお勧めします。

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

補足:

  • register関数を使用して、サーバー起動時にコードを実行できます。
  • スタンドアロン出力モードでは動作しないため、runtimeConfigオプションの使用はお勧めしません。代わりに、Appルーターを段階的に採用することをお勧めします。

キャッシュとISR

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

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

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

自動キャッシング

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

静的アセット

異なるドメインや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)
  })
}