3-3. Nginx - リバースプロキシ・ロードバランサ・静的配信
所要時間: 40-50分(がっつりなら2セッション分) ゴール: Nginxの3つの役割(リバプロ/LB/静的)を理解し、Go API への安全なフロントエンドとして本番投入できる設定を書ける コミット内容: 動作する nginx.conf を
~/learn/linux/day15にコミット
この章が終わるとできること
- Nginx の3つの役割(リバプロ / ロードバランサ / 静的配信)を区別できる
nginx.confのブロック構造(main / events / http / server / location)を書けるproxy_passで 末尾スラッシュの有無 の挙動の違いを説明できるproxy_set_headerの重要4ヘッダ(Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto)を暗記しているnginx -t→nginx -s reloadで 無停止反映 できるlimit_reqでレート制限を設定できる
Day 11 / Day 13 / Day 16 とのつながり
- Day 11 (systemd) で常駐させた Go サーバーの 前段 に立つのが Nginx
- Day 13 (ログ管理) の
access.log/error.logがここで本格運用 - Day 16 (ユーザー管理) で学んだ「専用ユーザー(nginx)」「最小権限」がここで活きる
- Day 5 (パイプ) の
awk '$9==404'が本物の Nginx ログ集計で使える
これができると何が嬉しいか
- 「Go の
http.ListenAndServeを 80番直接公開」の事故設計を絶対しなくなる - 「
reloadで無停止反映」「Blue/Green 切替」の基礎が手に入る - HTTPS化(Day 19以降)・Day 21 デプロイの土台が固まる
大前提: なぜ Web の前段にほぼ必ず Nginx がいるのか
「Webアプリ = アプリケーションサーバー単体」で済む時代は終わっています。 本番構成でほぼ100%、アプリの前段にリバースプロキシ(Nginx / Envoy / HAProxy / ALB など)が立っています。理由は:
- アプリの開発言語に依存しない共通の機能を切り出せる: HTTPS 終端、ヘッダ書き換え、レート制限、ログ
- 静的ファイルはアプリで配信するな: Go/Node/Rails より Nginx の方が桁違いに速い(C で書かれた専用サーバー)
- ロードバランシング: アプリインスタンスを複数並べて負荷分散 + 死活監視
- 段階的デプロイ: アプリ再起動中もリクエストを保持(Nginx がバッファ)
- セキュリティ層: 怪しいリクエストをアプリに到達する前に弾く
Nginx を知らないと「Go の http.ListenAndServe で80番直接公開」みたいな本番設計をしてしまう。これは性能的にもセキュリティ的にも事故です。今日は 本番で通用する Nginx 設定 を、設計思想込みで身に着けます。
セッション①: Nginx の本質と設定構造(25-30分)
0. 録画スタートと環境準備
mkdir -p ~/log ~/learn/linux/day15
cd ~/learn/linux/day15
script ~/log/linux_day15.log
# Mac でローカル動作確認するなら
brew install nginx
nginx -v
# nginx version: nginx/1.27.x
# Linux ならパッケージから
# sudo apt install nginx
# nginx -v
# 設定ファイルの場所
nginx -V 2>&1 | tr ' ' '\n' | grep conf1. Nginx が解決した問題(vs Apache)
Nginx の本質
2004年、Igor Sysoev(ロシア人エンジニア)が「C10K問題」を解決するために開発したWebサーバー。
C10K問題: 1台のサーバーで同時1万接続を処理する課題。当時の主流 Apache は接続ごとに1プロセス or 1スレッドを割り当てるため、メモリと context switch のコストで詰まった。
Nginx の解法: イベント駆動 + 非同期ノンブロッキングI/O。1つのワーカープロセスが数千〜数万の接続を1つのイベントループで捌く。
項目 Apache (prefork/worker) Nginx アーキテクチャ 1接続=1プロセス/スレッド 1ワーカー=多数の接続をイベントループ 同時接続スケール 数百〜数千 数万〜数十万 メモリ使用 接続数に比例 接続数にほぼ依存しない 静的ファイル配信 普通 圧倒的に高速 設定の柔軟性 .htaccessでディレクトリ別設定可能中央設定 モジュール 動的ロード可能(豊富) コンパイル時組み込みが主流 歴史: 2011年に商用版 NGINX Plus が登場、2019年にF5が買収。OSS版は今もコミュニティ運営されている。
2. Nginx の役割 3つ
静的配信 vs リバースプロキシ vs API ゲートウェイ
ざっくり言うと
Nginx の使い方には3つの典型パターンがあり、それぞれ「自分でレスポンスを返すか」「誰かに転送するか」「賢く中継するか」で別物。
パターン1: 静的ファイル配信(自前で返す)
クライアント ──→ [Nginx] ──→ ファイルシステム
└─ /var/www/html/index.html を読んで返す
location / {
root /var/www/html;
}イメージ: コンビニのおにぎり棚。在庫を直接渡すだけ。
パターン2: リバースプロキシ(裏のアプリに丸投げ)
クライアント ──→ [Nginx] ──→ [Go API on :8080]
└─ そのまま転送、レスポンスを返す
location /api/ {
proxy_pass http://127.0.0.1:8080;
}イメージ: 受付の取次。「ちょっとお待ちください」と裏に通す。
パターン3: API ゲートウェイ(賢い中継)
リバプロを発展させて、認証・レート制限・ヘッダ書き換え・ルーティングまで担う層。
クライアント ──→ [Nginx API Gateway] ──┬──→ [認証サービス]
├──→ [Go API]
└──→ [Node API]
└─ レート制限・JWT検証・ヘッダ書換
location /api/users/ {
limit_req zone=api burst=20 nodelay;
auth_request /auth;
proxy_pass http://users_service;
}
location /api/orders/ {
proxy_pass http://orders_service;
}イメージ: 空港のセキュリティチェック。「身分証は?」「持ち込み制限超えてない?」「行き先はあちらです」と賢く捌く。
対比表
| 静的配信 | リバースプロキシ | API ゲートウェイ | |
|---|---|---|---|
| 誰がレスポンスを作る | Nginx 自身 | 裏のアプリ | 裏のアプリ |
| 配信内容 | ファイル | アプリの動的レスポンス | アプリの動的レスポンス |
| 認証・レート制限 | 不要 | 任意 | 積極的にやる |
| ルーティング | パスマッチ | 単純転送 | サービスごとに分岐 |
| 典型ユースケース | SPA / 画像 | Go API への中継 | マイクロサービス前段 |
実際の1台 Nginx 構成
[クライアント]
│ HTTPS (443)
▼
[Nginx]──────────────────────┐
├─ /static/* → ファイル ← 静的配信
├─ /api/* → Go API ← リバプロ
└─ / → SPA (React) ← 静的配信(SPA fallback)
一番覚えやすい説明
- 静的配信 = 自分で答える
- リバプロ = 取り次ぐ
- API ゲートウェイ = 取り次ぎ + 警備員 + 受付
3. 設定ファイルの構造
Nginx で「どこをいじれば何が変わるか」を即座に判断するには、設定ファイルのディレクトリ構造を頭に入れるのが先決です。
- 何のコマンドか:
/etc/nginx/配下のディレクトリ階層(nginx.conf/conf.d//sites-available//sites-enabled//snippets/)の役割分担 - いつ使うか: 新しいサイトを追加する時、設定を一時停止する時、本番で問題が出て「どのファイルを見ればいいか」を判断する時
- 解決する具体的な問題: 「
nginx.conf1ファイル巨大化」を避け、サイトごと / 共通断片ごとにファイル分割して管理性を保つ
# Ubuntu/Debian 流(推奨)
/etc/nginx/
├── nginx.conf # メイン設定(最低限の枠組み)
├── conf.d/ # 追加設定(http ブロック内)
│ └── *.conf
├── sites-available/ # サイト定義の保管庫
│ ├── default
│ ├── api.example.com
│ └── www.example.com
├── sites-enabled/ # 有効なサイト(sites-availableへのsymlink)
│ ├── api.example.com -> ../sites-available/api.example.com
│ └── www.example.com -> ../sites-available/www.example.com
└── snippets/ # 共通設定の断片(SSL、proxy headers等)sites-available と sites-enabled の分離
Debian/Ubuntu の伝統的なパターン。
- sites-available/: サイト設定を書き溜める場所
- sites-enabled/: そのうち有効化したいものへのsymlink
サイトを一時停止したい時:
sudo rm /etc/nginx/sites-enabled/api.example.com # symlink削除(実体は残る) sudo nginx -s reload再開時は
ln -s ../sites-available/api.example.com /etc/nginx/sites-enabled/で復活。CentOS/RHEL / macOS の Homebrew:
conf.d/だけのフラットな構造が主流。どちらも本質は同じで「メイン設定 + 個別ファイル群」のパターン。
nginx.conf のブロック構造
# main コンテキスト(最上位) user www-data; worker_processes auto; events { # ワーカープロセスの挙動 worker_connections 1024; } http { # HTTP 共通設定 include /etc/nginx/mime.types; gzip on; # 個別サイト server { listen 80; server_name example.com; location / { root /var/www/html; } location /api/ { proxy_pass http://localhost:8080; } } }階層構造:
- main: グローバル設定(worker_processes、user)
- events: 接続処理(worker_connections)
- http: HTTPサーバー全体(共通設定)
- server: 1つの仮想ホスト
- location: URL パスごとの処理ルール
- upstream: バックエンドサーバー群の定義(ロードバランシング)
4. server ブロック - 仮想ホスト
1台の Nginx で複数のドメインを捌くには、server ブロックでドメインごとの仮想ホストを定義します。本番では1サーバーに5〜10サイト同居するのも普通です。
- 何のコマンドか:
server { listen ...; server_name ...; ... }で「このドメインに来たリクエストはこう処理する」を宣言する - いつ使うか: 新ドメインを追加する時、サブドメイン(api / www / admin)を分けて運用する時、不正な
Hostヘッダをdefault_serverで弾く時 - 解決する具体的な問題: 1IP で複数サイト運用、Host ヘッダによる仮想ホスト切替、IP 直接アクセスや不審 Host の遮断
server {
listen 80;
listen [::]:80; # IPv6
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
server_nameのマッチングルールNginx は HTTP リクエストの
Hostヘッダでserverブロックを選ぶ。優先順位:
- 完全一致:
server_name example.com;- 先頭ワイルドカード:
*.example.com- 末尾ワイルドカード:
example.*- 正規表現:
~^(www|api)\.example\.com$- default_server: どれにもマッチしない時
default_server の指定:
server { listen 80 default_server; server_name _; return 444; # 444: 接続を黙って閉じる Nginx 独自レスポンス }「IP直接アクセス」「不正なHostヘッダ」をデフォルトで弾く設定。本番で重要。
5. location と try_files
server ブロック内で「URL パスごとに違う処理」を分岐するのが location です。Nginx 設定の難所であり、ここの理解が浅いと 404 や予期せぬバックエンド転送が頻発します。
- 何のコマンドか:
locationは URL のパス(前方一致・完全一致・正規表現)に基づくルーティング、try_filesは「複数候補を順に試してフォールバック」する仕組み - いつ使うか: SPA のクライアントルーティングで「全部 index.html に流す」時、静的アセットを長期キャッシュさせる時、
/api/だけバックエンドに流す時 - 解決する具体的な問題: 「URL ごとにキャッシュ戦略やバックエンドを変えたい」「ファイルが無ければ別ファイルにフォールバック」を1ブロックで宣言的に表現する
# 完全一致
location = /favicon.ico {
log_not_found off;
}
# 前方一致(普通の動作)
location / {
try_files $uri $uri/ /index.html; # SPA用パターン
}
# 静的アセット(拡張子マッチ、正規表現)
location ~* \.(jpg|jpeg|png|gif|svg|webp|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 正規表現
location ~ ^/api/(.+)$ {
proxy_pass http://backend;
}location のマッチング順を物語で理解する
ざっくり言うと
location ブロックを複数並べた時、Nginx は決まった優先順位で「どれが当たるか」を判定する。記述順ではない。
まず素朴な疑問
location /api/ { proxy_pass http://backend; }
location ~ \.jpg$ { expires 30d; }
location = /favicon.ico { log_not_found off; }
location ^~ /static/ { root /var/www; }
location / { root /var/www/html; }/static/logo.jpg というリクエストが来た時、どの location が選ばれるか?
普通に上から評価すると思いがちだが、Nginx はそうしない。
Nginx の判定フロー
リクエスト URI 受け取る
↓
[ステップ1] = で完全一致するものはあるか?
├─ ある → 即決定(終了)
└─ ない ↓
[ステップ2] ^~ または 普通の prefix で「一番長くマッチ」するものを記憶
↓ (まだ決定ではない、暫定キープ)
[ステップ3] その暫定マッチが ^~ ならそこで確定(終了)
↓
[ステップ4] ~ または ~* の正規表現を「上から順に」試す
├─ マッチした → そっちを採用(終了)
└─ どれもマッチしない ↓
[ステップ5] ステップ2の暫定マッチを採用
例で追う: /static/logo.jpg の場合
[ステップ1] = で完全一致 → なし
[ステップ2] prefix で長くマッチ → /static/ (^~ 付き)
[ステップ3] ^~ なので確定 → location ^~ /static/ を使う
正規表現 ~ \.jpg$ はマッチするはずだが、^~ が付いた prefix が勝つので評価されない。
例で追う: /api/users の場合
[ステップ1] = で完全一致 → なし
[ステップ2] prefix で長くマッチ → /api/ (^~ なし)
[ステップ3] ^~ じゃないので暫定キープ
[ステップ4] 正規表現を試す → \.jpg$ にもマッチしない
[ステップ5] 暫定の /api/ を採用 → proxy_pass
優先順位の覚え方(4段階)
| 優先 | 記法 | 名前 | 意味 |
|---|---|---|---|
| 1 | location = /exact | 完全一致 | 最優先、終了 |
| 2 | location ^~ /prefix | 前方一致(正規表現スキップ) | これがマッチすれば正規表現は見ない |
| 3 | location ~ regex / ~* regex | 正規表現 | 上から順に評価 |
| 4 | location /prefix | 普通の前方一致 | 一番長くマッチしたものが採用 |
イメージ: 空港の優先搭乗順
== ファーストクラス(最優先で即通過)^~= ビジネス(正規表現の列に並ぶ必要なし)~= エコノミー(普通に並ぶ)/= エコノミー後方(誰もいなかったら案内)
一番覚えやすい説明
- 完全一致 → 前方一致(^
) → 正規表現() → 普通の前方一致 - 順序は 「強い記法から弱い記法へ」
proxy_pass vs try_files vs return の使い分け
ざっくり言うと
location の中で「何をするか」の主要3パターン:
proxy_pass= 裏のサーバーに転送try_files= ファイルを順に探して見つかったものを返すreturn= その場で即レスポンス
proxy_pass: 取り次ぎ
location /api/ {
proxy_pass http://127.0.0.1:8080;
}クライアント → Nginx → Go アプリ → Nginx → クライアント
(ここで処理)
イメージ: 受付の取次。
try_files: ファイルを順に試す
location / {
try_files $uri $uri/ /index.html;
# ① ② ③
}①リクエストされたファイルがあるか?
├─ あった → 返す
└─ ない ↓
②同名ディレクトリがあるか?
├─ あった → index ファイルを返す
└─ ない ↓
③/index.html を返す(SPA fallback)
イメージ: 棚を3つ順に開ける。「ない、ない、あった!」
return: その場で即レスポンス
location = /health {
return 200 "OK\n";
}
location = / {
return 301 https://www.example.com$request_uri;
}クライアント → Nginx → クライアント
(アプリにもファイルにも触れず即答)
イメージ: インターホン越しの即答。「はい OK」「あちらへどうぞ」。
対比表
| proxy_pass | try_files | return | |
|---|---|---|---|
| 何をする | 裏のサーバーに転送 | ファイルを順に探す | 即レスポンス |
| 必要なもの | upstream / URL | ファイル | ステータスコード |
| 典型用途 | API 中継 | SPA / 静的 / WP | ヘルスチェック / リダイレクト |
| アプリ動かす? | はい | いいえ | いいえ |
| ディスク I/O? | なし | あり | なし |
| 一番速いのは | 中 | 中 | 圧倒的に速い |
一番覚えやすい説明
- proxy_pass = 取り次ぐ
- try_files = 棚を順番に探す
- return = 即答する
rootとaliasの罠# root: location パスをそのまま付け足す location /static/ { root /var/www; # → /static/foo.png は /var/www/static/foo.png を探す } # alias: location パスを置き換える location /static/ { alias /var/www/assets/; # → /static/foo.png は /var/www/assets/foo.png を探す }これを混同すると404の原因になる。alias の場合は 末尾スラッシュの一致が必須。
セッション②: リバースプロキシ・LB・本番運用(25-30分)
6. リバースプロキシ - Go API への中継
Nginx の最重要ユースケースがリバースプロキシです。バックエンドアプリ(Go/Node/Rails/Python)への中継を、HTTPS 終端+ヘッダ転送+タイムアウト制御込みでこなします。
- 何のコマンドか:
upstreamでバックエンド先を定義し、proxy_passでリクエストを転送、proxy_set_headerで必要なヘッダを付け加える - いつ使うか: Go API を
127.0.0.1:8080に立てて Nginx 経由で公開する時、複数アプリインスタンスへの負荷分散をする時、HTTPS をフロントで終端してバックエンドに HTTP で渡す時 - 解決する具体的な問題: 「Go の
http.ListenAndServeで 80/443 直接公開」を避け、TLS・ヘッダ・タイムアウト・バッファ・ロギングといったインフラ関心事をアプリから剥がす
# /etc/nginx/sites-available/api.example.com
upstream go_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://go_backend;
# 必須ヘッダの転送
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# HTTP/1.1 + keepalive
proxy_http_version 1.1;
proxy_set_header Connection "";
# タイムアウト
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# バッファ
proxy_buffering on;
proxy_buffers 8 16k;
}
}
proxy_passの本質「来たリクエストを別サーバーに転送する」ディレクティブ。これだけで:
- クライアント → Nginx へのリクエストを受ける
- Nginx が新しい HTTP リクエストを構築してバックエンドへ送信
- バックエンドからのレスポンスをクライアントに返す
末尾スラッシュで挙動が変わる:
location /api/ { proxy_pass http://backend; # /api/users → http://backend/api/users } location /api/ { proxy_pass http://backend/; # /api/users → http://backend/users ← パスを書き換える }
proxy_set_header で転送する重要ヘッダ
ヘッダ 役割 Host元のホスト名(バックエンドが仮想ホスト判定に使う) X-Real-IPクライアントの実IP(バックエンドのログ・レート制限用) X-Forwarded-Forプロキシ経由のIP履歴( ,区切りでチェーン)X-Forwarded-Proto元のスキーム(http/https)。HTTPS終端後にバックエンドに httpで渡す時、アプリ側で redirect 判定に使うX-Forwarded-Host元のHost これを設定しないと: バックエンド側でクライアントIPが取れない(全部Nginxのlocalhostに見える)、HTTPSかHTTPか判別できずリダイレクトループ、などの事故が起きる。
X-Forwarded-Forを信用するな(重要)
X-Forwarded-Forはクライアントが自由に偽装できるヘッダ。悪意あるクライアント:
GET /admin HTTP/1.1 X-Forwarded-For: 127.0.0.1これを受けたバックエンドが「127.0.0.1 = ローカル管理者」と判定したら認証バイパスになる。
正しい運用:
- Nginx が
proxy_add_x_forwarded_forを使う → 既存のXFFに$remote_addrを追記する形になる- バックエンドは「最後尾のIP = 直前のプロキシのIP」「最後から数えて N 番目が信頼可能な実IP」と判定(プロキシ段数を知っている前提)
- または
X-Real-IPを使う(Nginx が直前のIPだけ書く)アプリケーション側で信頼するプロキシのIP範囲を明示する設定が必要(Go なら
httputil.ReverseProxyの前段で IP リストをチェック、Rails ならconfig.action_dispatch.trusted_proxies)。
7. ロードバランシング
アプリインスタンスを複数台並べた瞬間、Nginx の役割に「負荷分散」が加わります。同じ upstream ブロック内で複数サーバーを並べるだけで、Nginx が自動的に振り分けてくれます。
- 何のコマンドか:
upstreamブロック内に複数serverを列挙し、least_conn/ip_hash/ 重み付け(weight=)でアルゴリズムを切替える - いつ使うか: トラフィック増でアプリを3台に増やした時、特定インスタンスをメンテに入れる時(
downフラグ)、セッションをインスタンスに固定する時(ip_hash) - 解決する具体的な問題: 「1台では捌けない」「1台が落ちたら全部止まる」を、複数台 + 死活監視(
max_fails/fail_timeout)で解決する
upstream go_backend {
# ロードバランシング方式(デフォルト: ラウンドロビン)
# least_conn; # 接続数最少のサーバーへ
# ip_hash; # クライアントIPでハッシュ → 同じIPは同じサーバー(セッション固定)
# hash $request_uri; # URL でハッシュ
server app1.internal:8080 weight=3; # 3倍多くリクエストを送る
server app2.internal:8080;
server app3.internal:8080 backup; # 他が全部死んだ時だけ使う
# 死活検知パラメータ
server app4.internal:8080 max_fails=3 fail_timeout=30s;
keepalive 32;
}LB方式の使い分け
方式 特徴 使い所 round-robin(デフォルト) 順番に振る ステートレスAPI least_conn 接続数が少ない方へ 処理時間にばらつきが大きい場合 ip_hash クライアントIPで固定化 セッションがメモリにある古いアプリ hash $custom_var カスタムキー キャッシュサーバー振り分け 現代的ベストプラクティス: アプリはステートレスにして round-robin / least_conn を使う。セッションは Redis などの共有ストアに持つ。
ip_hashでセッションを固定化する設計は今は推奨されない(インスタンス追加時に分散が崩れる)。
8. 静的ファイル配信の最適化
Nginx の 真骨頂は静的ファイル配信の速さですが、デフォルト設定だと十分な性能が出ません。gzip 圧縮・キャッシュヘッダ・アクセスログ抑制の3点を入れて初めて本領発揮します。
- 何のコマンドか:
gzip onで転送量を減らし、expires/Cache-Controlでブラウザキャッシュを効かせ、access_log offで書き込みI/Oを削減する - いつ使うか: SPA の build 成果物を配信する時、画像 CDN を自前で立てる時、ハッシュ付きファイル名(
main.a8f3.js)に長期キャッシュを当てる時 - 解決する具体的な問題: 「Go や Node でファイル配信して CPU を食う」「キャッシュが効かず毎回フルダウンロード」を、Nginx 1台でまとめて解決する
server {
listen 80;
server_name www.example.com;
root /var/www/example.com;
# gzip 圧縮
gzip on;
gzip_vary on; # Vary: Accept-Encoding ヘッダ追加
gzip_min_length 1024; # 1KB以下は圧縮しない(オーバーヘッド回避)
gzip_types text/plain text/css text/xml
application/json application/javascript application/xml+rss
application/atom+xml image/svg+xml;
gzip_comp_level 6; # 1(速)〜9(高圧縮)、6が定番
# 静的アセットは長期キャッシュ
location ~* \.(jpg|jpeg|png|gif|webp|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off; # 静的ログは抑制(性能)
}
# CSS/JS は version 付き想定で長期キャッシュ
location ~* \.(css|js)$ {
expires 30d;
add_header Cache-Control "public";
}
# HTML はキャッシュさせない(最新版を毎回取得)
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# SPA フォールバック
location / {
try_files $uri $uri/ /index.html;
}
}gzip vs brotli
- gzip: 1992年〜、すべてのブラウザが対応、圧縮率は中庸
- brotli: 2015年Google、gzipより15-20%高圧縮、主要ブラウザ対応
Nginx で brotli を使うには
ngx_brotliモジュールが必要(標準では入っていない)。CDN/Cloudflare 経由なら勝手に brotli 化されることが多い。本番判断: Nginx 単体なら gzip でOK。CDN を前段に置く構成なら gzip も CDN に任せて Nginx は無効化することも。
Cache-Control: immutable の意味
ファイル名にハッシュ付き(
main.a8f3b9.js)の場合、そのURLは絶対に中身が変わらない。これにimmutableを付けると、ブラウザは期限内は再検証もしない(304 もない)。読み込みが最速化。ビルドツール(Webpack/Vite)でハッシュ付きファイル名が出る前提の最適化。
9. ログ設定とフォーマット
Nginx の強みの1つが詳細なアクセスログです。デフォルト形式でも有用ですが、JSON 形式 + upstream タイムを追加することで本番の障害調査が劇的に楽になります。
- 何のコマンドか:
log_formatでログのフィールドと書式を定義、access_log/error_logで出力先を指定する - いつ使うか: 「サイトが遅い」と言われて upstream の処理時間を見たい時、ログ集約基盤(Loki / Datadog / Fluentd)に流す時、特定エンドポイントの 5xx を集計する時
- 解決する具体的な問題: 「ログがあっても見方が分からない」「テキストパースに正規表現を書く手間」を、JSON 形式で一発解決する
# /etc/nginx/nginx.conf の http ブロック内
http {
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
# JSON フォーマット(ログ集約に流す時はこちら推奨)
log_format json escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
}ログを読む実務感覚
access.log: 全リクエスト記録。1日数GBになることも → logrotate 必須error.log: アクセスログでない異常(接続エラー、設定ミス、upstream タイムアウト)upstream_response_timeを見る: バックエンドが遅いか、Nginxの問題か切り分け$request_timeと$upstream_response_timeの差 = Nginx 自身の処理時間 + ネットワーク時間JSON フォーマット推奨理由: Fluentd / Vector / Promtail でログ集約する時、JSON ならパース不要でそのまま流せる。Grok 正規表現でパースする時代は過ぎつつある。
10. セキュリティ設定
Nginx を世界に晒す以上、最初に入れるセキュリティ設定は決まっています。これを入れないと、攻撃面が広い状態で公開され続けることになります。
- 何のコマンドか:
server_tokens off(バージョン秘匿)、各種add_header(HSTS / CSP / X-Frame-Options など)、ssl_protocolsで TLS バージョン制限、limit_reqでレート制限 - いつ使うか: 本番公開直前のチェック、セキュリティ診断対応、ログインAPIに厳しめのレート制限を入れる時、HSTS preload に登録する時
- 解決する具体的な問題: 「Nginx バージョン丸見え」「クリックジャッキング」「ブルートフォース攻撃」「巨大ボディ送信による DoS」を、設定数行で同時に塞ぐ
server {
listen 443 ssl http2;
server_name api.example.com;
# サーバー情報を隠す
server_tokens off; # nginx version を Server ヘッダから消す(http コンテキストでも可)
# セキュリティヘッダ
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'" always;
# SSL 設定(Mozilla Intermediate 推奨)
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTSにより HTTP は 301 で HTTPS に転送
# (別 server ブロックで listen 80 を作って return 301 https://$host$request_uri)
# レート制限
limit_req zone=api_limit burst=20 nodelay;
# body サイズ制限
client_max_body_size 10M;
}
# レート制限ゾーン定義(http コンテキスト)
# limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;レート制限
limit_req# http コンテキストでゾーン定義 limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; # IPで識別 ゾーン名 10MB 毎秒10req # server / location で適用 limit_req zone=api_limit burst=20 nodelay; # バースト20 待たせない
- rate=10r/s: 1秒あたり10リクエストを通す
- burst=20: 一時的に20まで貯められる(瞬間的なバーストを許容)
- nodelay: バーストを即時通す(無いと順次解放で遅延)
攻撃緩和とアプリ保護の両方に効く。ログインAPIには厳しめ、一般APIは緩めに設定するのが定石。
アンチパターン: 全部
location /で受ける# NG server { listen 80; location / { proxy_pass http://backend; } }静的ファイルもAPIも全部バックエンドに流す設計。バックエンドのCPU/メモリが浪費される。
# OK server { listen 80; location /static/ { alias /var/www/static/; expires 30d; } location /api/ { proxy_pass http://backend; } location / { root /var/www/html; try_files $uri /index.html; } }
アンチパターン: server_tokens を有効のまま本番運用
デフォルトだと:
Server: nginx/1.24.0がレスポンスヘッダに付く。Nginxのバージョンが分かる = 既知の脆弱性を攻撃者が当てやすい。
# nginx.conf の http ブロックで server_tokens off;これで
Server: nginxだけになる(完全に消すにはmore_clear_headersモジュールが必要)。
アンチパターン: デフォルト設定のまま本番
/etc/nginx/sites-enabled/defaultを放置して本番に出すと:
- default_server で何も返さない
- 不正Hostでアクセスされた時、適当なバックエンドに繋がってしまう
正解: default_server を明示的に作って
return 444;(接続を黙って閉じる)かreturn 421;を返す。
11. デバッグと運用
Nginx 設定は1文字のタイポでサービス停止に直結します。「設定変更 → reload」のフローを体に染み込ませることが、事故防止の最大の対策です。
- 何のコマンドか:
nginx -tで構文チェック、nginx -s reloadでゼロダウンタイム反映、nginx -Tでinclude展開後の全設定を確認 - いつ使うか: 設定編集後の必須チェック、本番でのリロード、設定が複雑になった時の全展開確認、
tail -fでリアルタイムログ追跡 - 解決する具体的な問題: 「設定ミスで Nginx が起動しなくなる」「再起動でユーザー接続が切れる」をそれぞれ
-tとreloadで回避する
# 構文チェック(必須)
sudo nginx -t
# 設定リロード(既存接続は維持される)
sudo nginx -s reload
sudo systemctl reload nginx
# プロセス確認
ps aux | grep nginx
# master + worker x N の構成が見える
# リアルタイムでアクセスログ追跡
sudo tail -f /var/log/nginx/access.log
# エラーログ
sudo tail -f /var/log/nginx/error.log
# 404 を集計
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# 設定の完全ダンプ(include 展開後)
sudo nginx -T
nginx -s reloadの挙動SIGHUP を master プロセスに送る。
- master が新しい設定を読む
- 新しい worker プロセスを起動
- 古い worker は 既存接続を処理し終わったら終了
ゼロダウンタイムで設定を反映できる。本番中でも問題なくreload可能(ただし
nginx -tで構文チェック必須)。
実例: Go API + 静的アセット(フル設定)
# /etc/nginx/sites-available/app.example.com
upstream go_app {
server 127.0.0.1:8080;
keepalive 32;
}
# レート制限ゾーン
# (これは http コンテキストに置くので /etc/nginx/conf.d/ratelimit.conf に分割)
# limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# HTTP は HTTPS にリダイレクト
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
# 本体(HTTPS)
server {
listen 443 ssl http2;
server_name app.example.com;
server_tokens off;
client_max_body_size 10M;
# SSL
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# セキュリティヘッダ
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# ログ
access_log /var/log/nginx/app.example.com.access.log json;
error_log /var/log/nginx/app.example.com.error.log warn;
# 静的アセット(ハッシュ付きファイル名想定)
location /static/ {
alias /var/www/app/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
gzip_static on; # 事前圧縮済み .gz を優先配信
}
# ヘルスチェック(ALB用などにバックエンドを介さず即200)
location = /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# ログインだけ厳しいレート制限
location = /api/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://go_app;
include /etc/nginx/snippets/proxy_headers.conf;
}
# 通常 API
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://go_app;
include /etc/nginx/snippets/proxy_headers.conf;
}
# SPA フォールバック
location / {
root /var/www/app/dist;
try_files $uri $uri/ /index.html;
}
}# /etc/nginx/snippets/proxy_headers.conf
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;練習課題
# 1. ローカルで Go の超軽量 HTTPサーバーを起動(仮想バックエンド)
# (Go = Google製のコンパイル言語。詳しくは「Go」章で扱う。
# ここでは「Nginx の裏に置く適当な HTTP サーバー」として使うだけ。
# Go が未インストールなら https://go.dev/dl/ から入手。
# 代わりに python3 -m http.server 8080 などでも可)
cd ~/learn/linux/day15
cat > server.go <<'EOF'
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go! Path=%s X-Real-IP=%s X-Forwarded-For=%s\n",
r.URL.Path, r.Header.Get("X-Real-IP"), r.Header.Get("X-Forwarded-For"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
EOF
go run server.go &
# 2. Nginx 設定を書く
mkdir -p static
echo "<h1>Static OK</h1>" > static/index.html
cat > nginx_local.conf <<'EOF'
worker_processes 1;
events { worker_connections 1024; }
http {
server {
listen 8090;
server_name localhost;
location /static/ {
alias /Users/takato/learn/linux/day15/static/;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
EOF
# 3. nginx を起動(フォアグラウンドで挙動確認)
nginx -t -c $(pwd)/nginx_local.conf
nginx -c $(pwd)/nginx_local.conf
# 4. テスト
curl http://localhost:8090/
curl http://localhost:8090/static/
# 5. ログ確認
tail -f $(brew --prefix)/var/log/nginx/access.log締め: git で証跡を残す
exit # script から抜ける
cd ~/learn/linux/day15
git init
git add nginx_local.conf server.go
git commit -m "feat(linux): Nginx リバプロ + 静的配信のローカル検証"チェックリスト
- Nginx と Apache のアーキテクチャの違いを1分で説明できる
-
nginx.confのブロック構造(main/events/http/server/location)を書ける -
proxy_passで末尾スラッシュの有無の挙動の違いを説明できる -
rootとaliasの違いを実例で書ける -
X-Forwarded-Forを盲目的に信用してはいけない理由を説明できる -
gzip設定でgzip_typesに含めるべきMIME型を3つ即答できる -
nginx -t→nginx -s reloadのフローを身に着けた -
server_tokens offの理由を説明できる - limit_req の rate/burst の使い分けを書ける
詰まった時のチートシート
| やりたいこと | コマンド/設定 |
|---|---|
| 構文チェック | sudo nginx -t |
| 設定再読み込み | sudo nginx -s reload |
| 完全再起動 | sudo systemctl restart nginx |
| 設定全ダンプ | sudo nginx -T |
| バージョン確認 | nginx -V |
| サイト有効化 | ln -s /etc/nginx/sites-available/X /etc/nginx/sites-enabled/ |
| サイト無効化 | rm /etc/nginx/sites-enabled/X |
| ログ追跡 | sudo tail -f /var/log/nginx/error.log |
| 404 集計 | awk '$9==404{print $7}' access.log | sort | uniq -c | sort -rn |
| upstream 設定 | upstream name { server ip:port; } |
| ヘッダ転送 | proxy_set_header X-Real-IP $remote_addr; |
「実務OK」基準
- Go/Node アプリへのリバプロ設定をゼロから書ける
- 静的アセットとAPIをパスで分離した設定ができる
proxy_set_headerの重要4ヘッダを暗記している: Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Protonginx -tを絶対打つ癖がついている- アクセスログから怪しいリクエストを見つけられる: 404多発、UA偽装、特定IPからの大量アクセス
- HSTS / CSP / X-Frame-Options の意味を即答できる: セキュリティヘッダ設計
- 「サイト遅い」と言われたら
upstream_response_timeを見る
さらに深掘るなら
- 公式ドキュメント: https://nginx.org/en/docs/ (ディレクティブ全網羅、英語だが読みやすい)
- 書籍: 『nginx実践入門』(技術評論社) - 日本語で深く学ぶならこれ
- Nginx ソースコード: https://github.com/nginx/nginx - イベントループ実装を読むと勉強になる
- 設定生成サイト: https://www.digitalocean.com/community/tools/nginx - GUI で nginx.conf を生成
- Mozilla SSL Config: https://ssl-config.mozilla.org/ - SSL設定のベストプラクティス
- Envoy / HAProxy / Caddy: Nginxの代替プロキシ。それぞれ思想が違うので比較で理解が深まる
アンチパターン / 初心者やらかし事例
NG 1: proxy_pass http://backend に末尾 / の有無で挙動が変わる罠
location /api/ {
proxy_pass http://backend; # → http://backend/api/users
proxy_pass http://backend/; # → http://backend/users(/api が消える!)
}→ 対策: 意図的に選ぶ。末尾 / あり = location の prefix を切り捨て、なし = そのままパス連結。
NG 2: X-Forwarded-For を盲信して認証に使う
# 攻撃者が `X-Forwarded-For: 127.0.0.1` を偽装すると「社内アクセス」扱いになる→ 対策: set_real_ip_from <trusted_proxy> で 信用できる送信元IPを限定。アプリ側でも追加検証。
NG 3: reload の代わりに restart で瞬断発生
sudo systemctl restart nginx # 既存接続が切れる→ 対策: 設定変更は sudo nginx -t && sudo nginx -s reload。reload は接続を保ったまま設定だけ再読み込み。
NG 4: 静的ファイルをアプリ経由で配信
location / { proxy_pass http://app; } # /static/ も全部アプリへ→ Go/Node より Nginx のほうが桁違いに速い。
→ 対策: location /static/ { root /var/www/; } で Nginx に直接配信 させる。
自己評価チェックリスト
- Nginx と Apache の イベント駆動 vs スレッド の違いを答えられる
-
nginx.confのブロック構造を空で書ける -
proxy_passの末尾/の有無の挙動の差を実例で言える -
proxy_set_headerの4ヘッダを覚えた -
nginx -tを打つクセが付いた
次のレッスン: Day 18 - ファイアウォール
明日は ファイアウォール ─ Nginx の前段、ネットワークレイヤーでの防御。
iptables / nftables / ufw / fail2ban、最小許可ポリシー、クラウドの Security Group との関係まで。今日 Nginx で「アプリの前段に立つ」を学んだので、その さらに前段 に何があるべきかを学ぶ。
→ Day 18: ファイアウォール へ