Nginx locationの優先順位:マッチング順序を徹底解説
nginxは location ブロックを上から順に読んで、最初に条件が合ったところで止まる、という動きはしない。「locationブロックが効かない」というバグ報告の大半は、この一つの誤解に行き着く。前方一致のlocationについては、ブロックをファイルのどこに置くかは結果にまったく影響しない。nginxはすべてを突き合わせたうえで、最も長く一致したものを覚えておく。
nginxの本当のlocation優先順位は、次の4段階に固定されている。
- 完全一致。 URIと
location = /pathが等しければ、nginxはそのブロックを使って探索を終える。前方一致の比較も正規表現も行わない。 - 最長の前方一致。 URIが先頭から一致するすべての前方一致location(
location /pathとlocation ^~ /path)を比較する。最も長いものは「記憶される」だけで、まだ使われはしない。 ^~によるショートカット。 記憶された前方一致に^~が付いていれば、nginxは正規表現の段階をまるごと飛ばし、そのブロックを使う。- 正規表現、ファイル記述順。 そうでなければ
~と~*のlocationが設定ファイルに現れた順に試され、最初に一致したものが勝つ。どれも一致しなければ、手順2で記憶した前方一致が使われる。
この4つのうち2つは、正反対の方向へ引っ張り合っている。前方一致は順序と無関係に長さで選ばれ、正規表現は長さと無関係に順序で選ばれる。設定を上から下へ読んでいる限り、この衝突が表に出ることはない。以下の例ではなく自分の設定ファイルについて答えが欲しいなら、無料の nginx location 優先順位テスター に貼り付ければいい。この手順をそのまま再現し、負けたブロックがどの段階で脱落したかを表示する。ツールはブラウザ内だけで動くので、貼り付けた本番設定がページの外に出ることはない。ここで説明したマッチング規則はすべて、二次的な記事から引き写したものではなく、稼働中の nginx 1.27.5 で実際に確認したものだ。
Nginx locationの優先順位を一覧で把握する
URIのマッチングに関与する修飾子は5つ、それに関与しないものが1つある。
| 修飾子 | 構文 | 一致の基準 | 正規表現の段階を止めるか | 主な用途 |
|---|---|---|---|---|
= | location = /path | 完全一致 | 止める | / や /favicon.ico のようなホットパス |
^~ | location ^~ /path | 前方一致 | 最長の前方一致だった場合のみ止める | 正規表現に絶対渡したくないディレクトリ |
~ | location ~ regex | PCRE、大文字小文字を区別する | 止めない | 大文字小文字が意味を持つ拡張子ルーティング |
~* | location ~* regex | PCRE、大文字小文字を区別しない | 止めない | 大文字小文字を問わない拡張子ルーティング |
| 「なし」 | location /path | 前方一致 | 止めない | パスによる一般的なルーティング |
@ | location @name | URIでは決して一致しない | — | error_page と try_files の飛び先 |
目安としては、完全一致がすべてに勝ち、正規表現は前方一致に勝ち、前方一致どうしは長さで勝負が決まる。唯一の例外が ^~ で、しかもその効き目は見た目より狭い。
この表はごく緩い意味でしか順位を表していない。表の上では ^~ が ~ より上に位置しているが、実際には ^~ のブロックが正規表現に負けることは珍しくない。修飾子が参照されるのは、すでに長さで勝った前方一致に対してだけだからだ。
選択アルゴリズムの4段階
nginxのlocationマッチング順序は、すべてのルールを一度に踏む設定を1つ読むのが早い。以下は nginxの公式ドキュメント にある例だ。
server {
location = / { return 200 "A\n"; }
location / { return 200 "B\n"; }
location /documents/ { return 200 "C\n"; }
location ^~ /images/ { return 200 "D\n"; }
location ~* \.(gif|jpg|jpeg)$ { return 200 "E\n"; }
}
5つのリクエストに対して、答えは5通りに分かれる。
| リクエスト | 勝者 | 理由 |
|---|---|---|
/ | A | 完全一致。ここで探索が即座に終わる。 |
/index.html | B | 正規表現がどれも一致せず、記憶していた前方一致が使われた。 |
/documents/document.html | C | / より長い前方一致。 |
/images/1.gif | D | ^~ が前方一致の段階で勝ったので、正規表現は動かなかった。 |
/documents/1.jpg | E | ^~ の付かない、より長い前方一致に正規表現が勝った。 |
肝は最後の2行だ。/images/ と /documents/ はどちらも前方一致で、どちらも一致し、どちらもそれぞれのリクエストにとって最長の一致だ。それでも片方は前方一致のブロックへ、もう片方は正規表現へ渡る。違いはたった2文字しかない。
手順2は見落とされやすい。nginxは最長の前方一致を「使う」のではなく「記憶する」。そのブロックはあくまで候補であり、正規表現の段階がリクエストを横取りできる。探索を終わらせるのは手順1、3、4だけだ。
「最長の前方一致」がパスの区切りではなく文字数で測られる理由
前方一致の比較はただの文字列比較だ。/ がパスの区切りであることを知らないし、区切りで止まりもしない。たとえば次の設定。
server {
location /static { }
location /static/ { }
}
/staticfoo へのリクエストは location /static が処理する。URIはこの7文字で始まっているので一致する。/static/ はそもそも一致しない。その位置にスラッシュがないからだ。/static/x へのリクエストは逆方向に進み、2つのうち長いほうである /static/ を取る。
その結果、location /static は /static-backup や /staticfiles など、たまたま同じ文字で始まるものすべてを抱え込む。ディレクトリを意図していたのなら、末尾のスラッシュを書いたうえで、スラッシュなしのパス用に完全一致のlocationを足す。
server {
location /static/ { root /var/www; }
location = /static { return 301 /static/; }
}
たいていのチュートリアルは、この挙動が表に出ない小綺麗なパスばかりを例に使う。だからこそコードレビューをすり抜けやすい。nginx location 優先順位テスター は一致したブロックをすべて、それぞれ何文字で一致したかとともに表示するので、兄弟パスを飲み込んでいる前方一致がその場で見て取れる。
^~ が実際に効いている場所
nginxのlocation ^~ 修飾子はよく「このブロックを正規表現より優先させるもの」と説明される。惜しいが、その惜しさが危ない。
^~ が本当にやっていることは、前方一致の比較の最中には何もしない、である。一致を長くするわけでも、どの前方一致が記憶されるかを変えるわけでもない。修飾子が参照されるのはそのあと、長さで勝ち残った1つの前方一致に対してだけだ。その勝者に ^~ が付いていれば正規表現の段階は飛ばされ、付いていなければ正規表現の段階は普通に走る。
つまり、より長い普通の前方一致があると、^~ は黙って無効化される。
server {
location ^~ /a/ { }
location /a/b/ { }
location ~ \.php$ { }
}
/a/b/x.php へのリクエストは ~ \.php$ が処理する。最長の一致は /a/b/ なので、nginxが記憶するのはそちらだ。修飾子なしなので正規表現の段階が許可され、正規表現が先に一致してリクエストを持っていく。^~ のブロックはファイルに残ったまま、守っているように見えたまま、このリクエストには何の影響も及ぼしていない。
URIを /a/x.php に変えると、同じ設定がまったく違う振る舞いをする。今度は ^~ /a/ が最長の一致になり、正規表現の段階は飛ばされ、^~ のブロックが勝つ。同じファイル、同じ形のリクエストで、結果は正反対になる。
この違いは実運用で効いてくる。^~ の典型的な用途は、書き込み可能なディレクトリをインタプリタから遠ざけることだ。
server {
location ^~ /uploads/ { }
location ~ \.php$ { fastcgi_pass unix:/run/php-fpm.sock; }
}
^~ を外すと、/uploads/evil.php へのリクエストはそのままPHP-FPMへ届く。アップロード経由のRCE報告が長年繰り返してきた形がこれで、脆弱か安全かを分けているのは2文字だ。「^~ が無効化される」ケースが重要なのもこのためで、location /uploads/thumbs/ のような長い普通の前方一致を1行足すだけで、その配下すべてで穴が開き直る。しかもそのdiffは、見た目には完全に無害だ。
適用範囲にも注意がいる。^~ が抑止できるのは、同じ階層で宣言された正規表現だけだ。自分のブロックの内側にネストされた正規表現は決して抑止しないし、ネストしたlocationに付けた ^~ はserverレベルで宣言された正規表現から守ってはくれない。nginx location 優先順位テスター で修飾子を付け外しすると、勝者が入れ替わる様子を追える。飛ばされた正規表現は表から消えず、飛ばされた旨がラベルとして残る。
正規表現のlocation:具体性より記述順
nginxのlocation正規表現は、大文字小文字を区別する ~ と、区別しない ~* の2種類だ。一方、前方一致はLinuxでは常に大文字小文字を区別する。(macOSのように大文字小文字を区別しないファイルシステム上では、nginxは前方一致も区別せずに比較し、そのうえですべての正規表現locationを ~* と同じ挙動に強制する。Macで開発してLinuxへデプロイしているなら、この違いは壊れたルールを本番に出るまで隠しかねない。)
引っかかるのはこのルールだ。正規表現は設定ファイルに現れた順に評価され、最初に一致した時点で探索が終わる。具体性も長さもアンカーの有無も、順序には一切影響しない。
server {
location ~ ^/a { }
location ~ ^/a/b/c$ { }
}
/a/b/c へのリクエストは ~ ^/a が取る。2つめのブロックはURIに完全に一致し、はるかに正確なのに、どんなリクエストに対しても永久に実行されない。nginx -t が何も言わずに受け入れる、死んだ設定だ。
だから習慣としては、正規表現を具体的なものから順に並べ、リストを短く保つ。広いパターンを上のほうに置くと、その下すべてが到達不能になる。行を並べ替えただけのdiffは無害に読めるので、この種のリグレッションは機能追加よりも整理整頓のときに紛れ込む。
アンカーにも落とし穴がある。location ~ /admin は両端どちらにもアンカーがないので、URIのどこでも探しにいき、/public/admin/x にも平気で一致する。先頭を意味したいなら ~ ^/admin と書く。~ \.php$ のように末尾だけアンカーするのは、拡張子ルーティングでは普通で正しい書き方だ。
JavaScriptの正規表現に慣れていると足をすくわれるPCRE特有の点がいくつかある。
- nginxはlocationのパターンを PCRE でコンパイルする。UTFモードもマルチラインモードも有効ではないので、パターンはバイト単位で動作し、
^はURIの先頭だけにアンカーされる。 - PCREの
$は、末尾の改行の直前にも一致する。%0Aで終わるURIでも\.php$を満たしてしまい、ファイル拡張子を鍵にしたルールをすり抜ける手口として知られている。 - JavaScriptに対応物のない構文もPCREには多い。アトミックグループ
(?>…)、絶対最大量指定子a*+、(?i)のようなインライン修飾子、[[:alpha:]]のようなPOSIXクラス、そして\A、\z、\K、\Q…\Eといったエスケープなどだ。 {や}を含むパターンは引用符で囲まなければならない。location ~ ^/a{2}$はunknown directive "2}$"で読み込みに失敗する。波括弧がトークンを終わらせてしまうからだ。location ~ "^/a{2}$"と書く。
キャプチャグループは期待どおりに動き、$1 以降をブロックの中で使える。
upstream backend {
server 127.0.0.1:8080;
}
server {
location ~ ^/user/(\d+)/profile$ {
proxy_pass http://backend/profiles/$1;
}
}
ファイル内での位置ではなくパターンそのものをまだデバッグしている段階なら、先に 正規表現テスター へ通す。構文の細部は 正規表現チートシート が詳しい。
誰もが飛ばす段階:URIの正規化
locationが参照されるより前に、nginxはリクエストターゲットを書き換えている。パターンが突き合わされるのは正規化後のパスであって、ワイヤ上を流れてきたバイト列ではない。解説記事がここに触れることは少ないが、「locationが一致しない」という相談の答えは、しばしばこの段階で決まっている。
正規化がやることは4つある。クエリ文字列を切り離し、パスをパーセントデコードし、. と .. のセグメントを解決し、連続するスラッシュをまとめる。
| リクエストターゲット | 正規化後の $uri | 補足 |
|---|---|---|
//a//x | /a/x | 連続したスラッシュが1つにまとめられる |
/a/../b/x | /b/x | マッチングの前に .. が解決される |
/a/b%2F..%2Fzz | /a/zz | %2F が本物の区切り文字にデコードされ、解決に加わる |
/a/%2e%2e/b/x | /b/x | %2E がドットにデコードされ、これも解決に参加する |
/a%20b/x | /a b/x | %20 が本物の空白になる |
/a+b/x | /a+b/x | パスの中では + は空白ではない |
/a?x=/b | /a | クエリ文字列が先に切り離される |
/a%3Fx=1 | /a?x=1 | %3F はリテラルのまま残り、クエリ文字列は空になる |
デコードされたあと再解釈されずにそのまま書き出される例外が3つある。%25、%23、%3F だ。/a%3Fx=1 のパスの中に疑問符が残り、$args が空になるのはこのためだ。
そもそもlocation選択まで到達しないターゲットも2種類ある。ルートより上へ登る .. セグメントと、%00 のような不正なエスケープは、どちらもマッチングが始まる前に 400 で拒否される。
実害が出るのはアクセス制御の文脈だ。locationブロックを境界として使っているなら、書いたパスは解決後のパスと突き合わされる。
server {
location /a/ { }
location /b/ { }
}
/a/b%2F..%2Fzz へのリクエストは /a/b/ の配下にとどまらない。/a/zz に正規化され、location /a/ が処理する。ここで $uri ではなく生のターゲットを前提に考えると答えを間違えるし、アクセス制御の文脈での「間違った答え」には固有の呼び名がある。location /admin に何かを守らせる前に、正規化後のパスが実際どうなるかを確かめておく。nginx location 優先順位テスター は生のターゲット、正規化後の $uri、切り離されたクエリ文字列を別々の行として表示する。エンコードそのものだけを確かめたいときは、URLエンコード・デコード が単体で処理してくれる。
もう1つはっきりさせておく。クエリ文字列はマッチングに一切関与しない。location /search?q= は /search?q=1 へのリクエストに一致できない。選択の段階で見えているのは /search だけだからだ。パラメータで分岐したいなら、ブロックの中で $arg_name を読む。
思ったとおりに動かない設定5例
より長い普通の前方一致に負ける ^~ ブロック
症状: ^~ を付けたディレクトリは守られているように見えるのに、その配下のリクエストを正規表現が処理してしまう。
原因: ^~ は、長さですでに勝った前方一致に対してしか参照されない。より長い普通の前方一致のほうが記憶され、そちらは何も抑止しない。
対処: 長いほうの前方一致にも ^~ を付けるか、長いほうの前方一致を削る。
# Broken: /a/b/x.php goes to the regex
location ^~ /a/ { }
location /a/b/ { }
location ~ \.php$ { }
# Fixed: /a/b/x.php goes to ^~ /a/b/
location ^~ /a/ { }
location ^~ /a/b/ { }
location ~ \.php$ { }
末尾スラッシュのない前方一致が兄弟パスを巻き込む
症状: 特定のディレクトリ向けに書いたブロックが、頭の文字が同じだけのパスまで処理してしまう。
原因: 前方一致はパスの区切りではなく文字を比較するので、location /app は /application にも一致する。
対処: 末尾のスラッシュを書く。スラッシュなしのパスも扱う必要があるなら location = /app を足す。
広い正規表現の下に置かれた具体的な正規表現
症状: 正確に書いたルールが一度も発火せず、エラーもどこにも出ない。 原因: 正規表現はファイル記述順に試され、最初の一致で探索が終わるため、広いパターンより下はすべて到達不能になる。 対処: 具体的なパターンを広いパターンより上へ移すか、広いほうにアンカーを足して絞る。
^~ のあとに正規表現を書く
症状: deny ルールがエラーなく読み込まれるのに、何もブロックしない。
原因: ^~ が取るのはリテラルの前方一致であって、パターンではない。nginxは文句を言わず、そのブロックがURIに一致することが単に一度もない。
対処: 正規表現の修飾子を使う。
# Broken: matches nothing, loads without error
location ^~ "\.php$" { deny all; }
# Fixed
location ~ \.php$ { deny all; }
クエリ文字列が関与すると思い込む
症状: ? を含むlocationが一度も一致しない。
原因: クエリ文字列は正規化の段階で切り離され、location選択はパスだけを相手に走る。
対処: パスで一致させ、ブロックの中で $arg_name を調べる。
location /search {
if ($arg_q = "") { return 400; }
}
デバッグ:実際に勝ったブロックを突き止める
最も確実な答えをくれるのはデバッグログだ。ただしデバッグサポート付きでビルドされたバイナリが要るので、まず確認する。
nginx -V 2>&1 | grep -o with-debug
有効にしたうえで、選ばれたブロックを名指ししている行をgrepする。
error_log /var/log/nginx/debug.log debug;
grep "using configuration" /var/log/nginx/debug.log
レスポンスヘッダーを使う手はもっと手軽で、デバッグビルドもいらない。候補それぞれに目印を付けて、返ってきたヘッダーを読む。
location ^~ /uploads/ {
add_header X-Debug-Location "uploads-caret" always;
return 204;
}
location ~ \.php$ {
add_header X-Debug-Location "php-regex" always;
return 204;
}
curl -sI --path-as-is 'http://localhost/uploads/evil.php' | grep -i x-debug-location
--path-as-is は重要だ。これがないとcurlが気を利かせて .. を解決してしまい、意図したURIとは別のURIを試すことになる。もっと込み入ったコマンドを組み立てるなら、cURLコマンドジェネレーター がフラグを書いてくれるし、残りは curl チートシート が扱っている。調査の結果として自分のヘッダーではなくリダイレクトや 404 が返ってきたときは、HTTPステータスコード早見表 を見れば、たいていどのモジュールが返したのか見当がつく。
nginx -T は include ファイルも含め、マージ後の設定を丸ごと出力する。6つのファイルが組み上がったあとで正規表現が実際どんな順序になっているかは、これで分かる。編集していたファイル上の並び順と一致することは、めったにない。
nginx -T | grep -n "location"
サーバーを触る前に、nginx location 優先順位テスター で失敗を再現しておく。デプロイしていない設定で試行を回すほうがリロードを繰り返すより速く、判定テーブルには各ブロックがどの段階で脱落したかが名指しで出る。
ネストしたlocationと try_files の位置づけ
ネストしたlocationは、1階層下で同じアルゴリズムを走らせる。前方一致のlocationが勝つと、nginxはその子へ降りて同じ探索を繰り返す。つまりネストした正規表現は、親階層の正規表現より先に試される。
server {
location ~ \.php$ { }
location /a/ {
location ~ \.php$ { return 200 "nested\n"; }
}
}
/a/x.php はネストしたブロックが処理する。ネストにはもう1つ副作用がある。全体で見れば長いはずの前方一致を到達不能にしうる、というものだ。各階層で勝ったものにしか降りていかないからである。location /a/bb/ が location /a/ の内側にネストしていて、外側の階層に兄弟として location /a/b がある場合、/a/bb/x へのリクエストは /a/b へ行く。外側の比較が先に起こり、そこで /a/b が勝つからだ。降りた先で何も見つからなくてもバックトラックはしない。親がそのままリクエストを保持する。
try_files と rewrite は選択の一部ではない。すでに勝ったブロックの内側で実行されるだけで、さかのぼって選択結果を変えることはできない。try_files を書いたブロックにそもそもリクエストが届かないなら、そのディレクティブは無関係だ。よくある犯人は、前方一致のブロックに順番が回る前にリクエストをさらっていく ~ \.php$ の正規表現である。例外が1つある。内部リダイレクト(rewrite … last や error_page によるジャンプ)はマッチングをやり直すので、書き換え後のURIはlocationのリストの先頭から改めて解決される。
最後に、末尾スラッシュなしでディレクトリを要求したときに返る 301 は、マッチングの失敗ではない。これを生む仕組みは2つある。名前が / で終わるlocationに proxy_pass などの *_pass 系ディレクティブが付いている場合、スラッシュなしの同じパスへのリクエストは選択の段階で 301 を返される。正規表現が評価されるより前に、そしてクエリ文字列を保ったままだ。
server {
location /user/ { proxy_pass http://backend/; }
}
# GET /user?x=1 -> 301 to /user/?x=1
location = /user を足すと、このリダイレクトは抑止される。これとは別に、静的ファイルモジュールは、パスがディスク上の実在のディレクトリに解決されたときに独自の 301 を出す。こちらは設定ではなくファイルシステム次第だ。
FAQ
nginxのlocation修飾子5つとは何か
nginxのlocation修飾子は5つある。完全一致の =、正規表現の段階を飛ばす前方一致の ^~、通常の前方一致である修飾子なし、大文字小文字を区別する正規表現の ~、区別しない ~* だ。6つめの形として location @name があるが、これはURIのマッチングに一切参加せず、error_page と try_files の飛び先としてのみ存在する。
= の完全一致はnginxを速くするのか
完全一致のlocationは探索を即座に終わらせ、前方一致の走査と正規表現の評価をすべて省く。節約は本物だが、体感できるほどの大きさではない。ヘルスチェックや /favicon.ico のように毎秒何千回も叩かれるエンドポイントには書く価値がある。普通のページのために = ブロックを大量に積み上げると、得られるものより設定の複雑さのほうが高くつく。
~*^/api のように空白なしで修飾子を書けるか
書ける。location ~*^/api/ と location ~* ^/api/ はまったく同じ意味だ。nginxは名前の先頭から修飾子を剥がし、長い修飾子から先に照合するので、~ より前に ~* が認識される。それでも空白は入れておくほうがいい。くっついた修飾子はパターンの一部のように読めて、レビュー中に人間のほうが読み違える。
locationブロックの中の root と alias の違いは何か
root はURI全体をディレクトリに連結し、alias は一致した前方一致部分をディレクトリで置き換える。location /static/ { root /var/www; } の場合、/static/x.css へのリクエストは /var/www/static/x.css を探す。alias /var/www/assets/; に変えると /var/www/assets/x.css を探す。alias を使うときは、locationとパスの両方に末尾スラッシュを付けるか、両方とも付けないかのどちらかにする。
1つのリクエストが複数のlocationブロックに一致することはあるか
一致するブロックは複数ありうるが、リクエストを処理するのはちょうど1つだけだ。nginxはすべての前方一致locationを、必要ならすべての正規表現locationも比較したうえで、ただ1つの勝者にリクエストを渡す。負けたブロックからディレクティブが継承されることはない。どこでも必要な設定は server か http の階層に置くか、各ブロックで書き直すしかない。
nginx -t はどのlocationが一致するか教えてくれるか
教えてくれない。nginx -t が確認するのは構文と設定の妥当性であって、リクエストをシミュレートすることは一切ないので、マッチング順序については何も報告しない。あるURIをどのブロックが持っていくのかを知るには、デバッグログを読むか、一時的なレスポンスヘッダーを足すか、設定を nginx location 優先順位テスター に貼り付けて、各ブロックが勝った理由と負けた理由を読む。