問題の概要
SPA(シングルページアプリケーション)やLaravelプロジェクトのデプロイを終えたばかりだとします。すべてが完璧に見えますが、ページをリロードした途端に「500 Internal Server Error」が発生します。Nginxのエラーログを確認すると、次のような特有の悩ましいエラーが見つかるはずです:
rewrite or internal redirection cycle while processing "/index.html"
これはNginxがループに陥っているために発生します。Nginxはファイルを探し、見つからないとフォールバック(代替手段)にジャンプしますが、そのフォールバックが全く同じ検索ロジックをトリガーしてしまいます。Nginxには内部リダイレクトの回数に10回という上限があります。11回目の試行に達すると、CPUの過負荷を防ぐためにリクエストを強制終了します。
環境
- OS: モダンなLinuxディストリビューション (Ubuntu 22.04+, Debian, RHEL)
- ソフトウェア: Nginx (全バージョン)
- 一般的なシナリオ: React/Vueでのクライアントサイド・ルーティング、PHPフレームワークでのindex.phpルーティング、またはカスタム404ハンドラー。
ループが発生する仕組み
通常、問題は楽観的すぎる try_files ディレクティブから始まります。ViteベースのReactアプリでよく見られる(しかし欠陥のある)設定を見てみましょう:
server {
listen 80;
root /var/www/my-app/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
これはNginxに対し、まずリテラルファイルを確認し、次にディレクトリを確認するように指示しています。両方が見つからない場合は、index.html を提供します。
罠は単純です: もし /dist フォルダに index.html が実際に存在しない場合(ビルドの失敗などが原因)、Nginxはそこで止まりません。/index.html へリダイレクトし、それが再び location / ブロックをトリガーします。検索して失敗し、2回目の /index.html へのリダイレクトが発生します。これがNginxが10サイクルの制限に達して諦めるまで繰り返されます。
ステップバイステップのデバッグ
1. エラーログの確認
ログをリアルタイムで監視して、どのパスが失敗しているかを特定します。ブラウザをリロードしながら、次のコマンドを実行してください:
tail -f /var/log/nginx/error.log
エラー内のパスに注意を払ってください。もし while processing "/index.php" と表示されているなら、Nginxは index.php がチェーン内の欠落しているリンクであることを伝えています。
2. ファイルパスと権限の確認
フォールバックファイルが、想定した場所に実際に存在するか確認します。ルートパスに対して ls を実行してみましょう:
ls -la /var/www/my-app/dist/index.html
ファイルが存在していても、Nginxがその読み取りをブロックされている可能性があります。namei を使用して、権限チェーン全体を確認します。/ からファイルに至るまでのすべてのディレクトリが、www-data ユーザーによって実行可能(+x)である必要があります:
namei -om /var/www/my-app/dist/index.html
3. 安全のために名前付きロケーションを使用する
名前付きロケーション(Named locations)は、よりクリーンな代替案です。ファイル確認ロジックと実際のフォールバック実行を分離し、偶発的な再帰を防ぎます。これはSPAを扱う際の業界標準の方法です。
SPA (React/Vue/Vite) の場合:
location / {
try_files $uri $uri/ @fallback;
}
location @fallback {
rewrite ^ /index.html break;
}
PHP (Laravel/WordPress) の場合:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
PHPの設定では、多くの場合 .php ブロック内でループが発生します。サイクルを止めるには、PHPハンドラー内の try_files ディレクティブに =404 を追加します。これにより、index.php が存在しない場合に、Nginxはループせずにクリーンな404を返すようになります。
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
最終的な解決策:堅牢な設定
シンプルな try_files 設定を好む場合は、最後のパラメータが新しい検索をトリガーしないようにする必要があります。以下は、モダンなウェブアプリ向けの検証済み設定です:
server {
listen 80;
root /var/www/html;
index index.html;
location / {
# ファイル、次にディレクトリを試行します。
# 両方が失敗した場合は、リクエストを index.html に送信します。
try_files $uri $uri/ /index.html;
}
# ループの可能性を断つために、index.html を明示的に処理します
location = /index.html {
# 'internal' ディレクティブは、ファイルが存在しない場合に
# 外部ユーザーがこのロケーションを直接呼び出すのを防ぎます。
internal;
}
}
検証
作業を終える前に、必ずNginxの構文を検証してください。セミコロンが1つ欠けているだけで、サイト全体がダウンする可能性があります。
sudo nginx -t
もし syntax is ok と表示されたら、変更を適用します:
sudo systemctl reload nginx
最後に、curl を使って存在しないルートをテストします。500 エラーの代わりに 200 OK(SPAのフォールバック)または 404 が返ってくれば、ループの解消に成功しています:
curl -I http://localhost/this-route-does-not-exist
学んだ教訓
- フォールバックはリダイレクトである:
try_filesの最後の項目は内部リダイレクトです。そのファイルが欠落していると、検索がやり直しになります。 - ルートのタイポは致命的:
rootパスを再確認してください。Nginxが/var/www/htmlを探しているのに、ファイルが/var/www/distにある場合、永久にループします。 - 名前付きロケーションの方が安全: 複雑なルーティングでは
@name構文を使用して、ロジックをクリーンに保ちましょう。 - 権限が重要: Nginxは読み取れないファイルを提供することはできません。
www-dataがファイルの所有権を持っていることを確認してください。

