リバースプロキシの積み重ね:本番アーキテクチャ
1 つのアプリケーションの前に 4 つのリバースプロキシを置くことは、一見するとシステムの簡略化が必要な状態のように見えます。本番 Web インフラを扱うほとんどのチュートリアルは 1 層で落ち着き — Nginx、Caddy、Traefik のいずれか — それ以上はオーバーエンジニアリングとして扱います。しかし現実には、これらのツールはそれぞれ異なる問題を解決しており、分業を理解すれば、見かけの冗長性は一貫した構造として整理されます。
これは私たちが複数のクライアントデプロイメントで運用しているアーキテクチャです:エッジに Cloudflare、コンテナ対応のエントリポイントとして Traefik、HTTP キャッシングに Varnish、アプリケーションホストとして Nginx、そしてチェーンの末端にアプリケーション本体。リクエストの流れは:
Cloudflare → Traefik → Varnish → Nginx → Application
以下では、各層が何を担うか、境界がどこにあるか、パフォーマンスへの影響 — よりシンプルな構成では発生しないオーバーヘッドが生じる箇所も含めて — を説明します。
各層が実際に担う役割
Cloudflare はパブリックインターネットのエッジで動作します。DDoS トラフィックを吸収し、WAF ルールを適用し、グローバルに分散した PoP で TLS を終端し、静的アセットをエンドユーザーの近くにキャッシュします。コンテナのインテリジェントなルーティング判断や、Cookie の検査が必要なアプリケーションレスポンスのキャッシュは得意ではありません。それは別の層の仕事です。
Traefik はインフラ内部のコンテナ対応エントリポイントです。マルチテナント Docker スタックを運用する際、Traefik の価値はコンテナラベルを読み取ってルーティングする点にあります — コンテナが再起動やスケールしても同期が崩れる手動設定ファイルは不要です。内部ドメインやステージングドメイン向けに Let's Encrypt の TLS 証明書を管理し、ヘッダーのストリップや書き換えを行い、IP 許可リストやベーシック認証などのミドルウェアチェーンを適用します。Traefik はキャッシュしません。負荷下での圧縮も得意ではありません。ルーティングとミドルウェアがその仕事であり、それを効率的にこなします。
Varnish は HTTP アクセラレーターです。エントリポイントとアプリケーションホストの間に置き、読み込みトラフィックを吸収します。適切にチューニングされた Varnish インスタンスは、1 ミリ秒以下で完全な HTML ページを返しながら、毎秒数万リクエストをメモリから処理できます。VCL(Varnish Configuration Language)は、何をどれだけの期間、どのような条件でキャッシュするかを細かく制御できます — Cloudflare のキャッシュルールよりもはるかに細かい制御が可能です。これは認証済みアプリケーションでユーザーごとにキャッシュ可能なレスポンスがある場合や、クエリパラメータによって異なる TTL を持つべきページに重要です。
Nginx はこのスタックでは、元々設計された役割を果たします:ファイルの提供と上流アプリケーションプロセスへのプロキシ。CloudPanel は Nginx をマネージド Web サーバーとして使用しており、各バーチャルホストには PHP-FPM、静的アセット配信、gzip 圧縮を処理する自動生成設定があります。その設定は CloudPanel が管理するため直接編集すべきではありません。ここでの Nginx は意思決定層ではなく、リクエストを受け取り、アプリケーションにプロキシし、レスポンスを返すだけです。
Traefik 設定:コンテナラベル
このスタックの Traefik ルーティングは、Docker Compose ラベルで完全に設定されます。Traefik の背後に Varnish を置く際の重要なポイントは、Traefik が Nginx ではなく Varnish コンテナに転送すべきだということです。多くの人が混乱する部分です:標準的な Traefik の例はアプリへの直接パターンを示しており、そのフローに Varnish を組み込む方法は十分に文書化されていません。
services:
varnish:
image: varnish:7.5
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`www.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=80"
- "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Port=443"
- "traefik.http.routers.myapp.middlewares=myapp-proto"
networks:
- traefik-public
- internal
X-Forwarded-Proto ヘッダーは重要です。これがなければ、アプリケーションは各リクエストを HTTP として認識します — Varnish と Nginx の通信は Docker 内部ネットワーク上でプレーン HTTP で行われるため — HTTPS を強制するアプリケーションでリダイレクトループが発生します。Traefik はリクエストが内部ネットワークに入る前にヘッダーを付与し、Varnish はそれをそのまま通過させ、Nginx はアプリケーションに転送します。X-Forwarded-Port のペアヘッダーは、ポートも検査するアプリケーションで同種のリダイレクトループを防止します。
もう 1 つの考慮点:Traefik の passHostHeader 設定はデフォルトで true であり、Varnish は元の Host ヘッダーを受け取ります。これが望ましい動作です — Varnish の VCL がキャッシュキーに使用でき、Nginx がバーチャルホストの選択に使用します。これを上書きすると、Varnish と Nginx が誤ったホストを受け取り、追跡困難な方法で静かに失敗します。
Varnish VCL:キャッシュロジック
以下の VCL は、CloudPanel の背後にある WordPress または PHP アプリケーション向けの実用的な出発点です。一般的なケースをカバーします:ログイン済みユーザーと非冪等リクエストのバイパス、静的アセットの拡張キャッシュ、トラッキングパラメータの正規化、キャッシュスタンピード防止のためのグレース期間。
vcl 4.1;
backend default {
.host = "nginx"; # service name in docker-compose
.port = "80";
.connect_timeout = 5s;
.first_byte_timeout = 30s;
.between_bytes_timeout = 10s;
}
sub vcl_recv {
# Pass admin and authenticated requests
if (req.url ~ "^/wp-(admin|login|cron|json)" ||
req.http.Cookie ~ "wordpress_logged_in_" ||
req.http.Cookie ~ "woocommerce_items_in_cart") {
return(pass);
}
# Only cache GET and HEAD
if (req.method \!= "GET" && req.method \!= "HEAD") {
return(pass);
}
# Normalise tracking parameters out of the cache key
if (req.url ~ "(?|&)(utm_source|utm_medium|utm_campaign|fbclid|gclid)=") {
set req.url = regsuball(req.url,
"&(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+", "");
set req.url = regsuball(req.url,
"?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+&", "?");
set req.url = regsub(req.url,
"?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+$", "");
}
# Remove cookies for static assets — allows caching
if (req.url ~ ".(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(?.*)?$") {
unset req.http.Cookie;
}
return(hash);
}
sub vcl_backend_response {
# Cache static assets for 7 days
if (bereq.url ~ ".(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(?.*)?$") {
set beresp.ttl = 7d;
set beresp.grace = 24h;
unset beresp.http.Set-Cookie;
return(deliver);
}
# Cache HTML pages for 5 minutes with a 60-second grace period
if (beresp.status == 200 && beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 300s;
set beresp.grace = 60s;
}
# Never cache error responses
if (beresp.status >= 500) {
set beresp.ttl = 0s;
}
return(deliver);
}
sub vcl_deliver {
# Expose cache status for debugging
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
# Remove server identity headers
unset resp.http.Via;
unset resp.http.X-Varnish;
return(deliver);
}
vcl_recv のトラッキングパラメータ正規化は見落としやすく、怠ると高コストになります。Cloudflare はエッジでいくつかのパラメータを除去しますが、UTM 値と fbclid トークンは元の URL のまま Varnish に到着します。正規化なしでは、Facebook 広告のクリックごとに、アプリケーション観点では同じページに対して一意のキャッシュエントリが生成されます。キャンペーン期間中、これにより Varnish キャッシュが何千もの Cold エントリに断片化し、TTL 設定に関わらずヒット率の向上効果が完全に失われます。
vcl_backend_response の grace 設定も詳細に理解すべきメカニズムです。キャッシュされたオブジェクトの TTL が切れると、通常次のリクエストは Nginx が新鮮なレスポンスを返すまで待つことになります。トラフィックスパイク時 — ニュースレター配信やソーシャルメディアへの言及 — 期限切れエントリに対して複数のリクエストが同時に到着します。グレースなしでは、各リクエストが並行して Nginx を通過します。60 秒のグレース期間があれば、Varnish はバックグラウンドフェッチが 1 件だけキャッシュを更新する間、他のすべてのリクエストに古いオブジェクトを提供します。結果として、期限切れの瞬間に Nginx が処理するのは数百件ではなく 1 件です。
パフォーマンスへの影響
このスタックのパフォーマンスは一様ではありません。優れた点もあれば、測定可能なオーバーヘッドが発生する点もあります。
優れている点:Varnish のメモリからキャッシュ可能なページが提供される場合、Traefik レベルで 2ms 以下で返ります。PHP から生成に 400ms かかる WordPress ホームページは、TTL が切れるまで後続のすべてのリクエストで 1〜2ms で提供されます。スループットの改善は段階的ではありません — 桁違いです。Nginx を通じた静的アセット配信はいずれにせよ速く、Cloudflare がエッジでそれらをキャッシュするため、本番環境では Nginx がそれらを処理することはほとんどありません。
オーバーヘッドが発生する点:キャッシュミスは 1 ホップではなく 4 ホップを経由します。キャッシュできない PHP リクエスト — ログイン済みユーザーセッション、POST 送信、WooCommerce カートページ — は PHP-FPM に到達する前に Traefik、Varnish、Nginx を通過します。同一ホスト上の各 Docker ネットワークホップは約 0.1〜0.3ms のレイテンシを追加します。50ms の PHP レスポンスには許容範囲です。5ms 以下で応答すべき API エンドポイントには許容できません。その場合、正しいアプローチは別の Traefik ルータールールを使って API パスをアプリケーションコンテナに直接ルーティングし、Varnish を完全にバイパスすることです。
メモリサイジングは他の多くのパラメータより重要:Varnish はオブジェクトストアを RAM から割り当てます。デフォルト設定は 256MB です。一意の URL が多いサイトではすぐに満杯になり、エビクション率が上昇し、TTL 設定に関わらずキャッシュヒット率が低い状態が続きます。デプロイ後最初にチューニングすべきパフォーマンスレバーは割り当ての増加です — -s malloc,2G 起動フラグ、または Docker の VARNISH_SIZE=2G 環境変数 — そして varnishstat -1 | grep hit で結果を監視します。
SSL 終端:TLS は Cloudflare で終端されます。Cloudflare フロントの構成では、Traefik〜Varnish および Varnish〜Nginx 間のセグメントは Docker 内部ブリッジネットワーク上でプレーン HTTP で動作します。これは意図的です。同一ホスト上の内部コンテナ通信に TLS を追加すると、意味のあるセキュリティ改善なしに CPU オーバーヘッドが発生します — Docker ブリッジトラフィックの脅威モデルはパブリックインターネットトラフィックとは根本的に異なります。コンテナ間通信の暗号化がスレットモデルに必要な場合、それは別のアーキテクチャの議論です。
このスタックが適切な場面
これは汎用の推奨事項ではありません。キャッシュ要件がなく CloudPanel の制約もない単一コンテナ化アプリケーションであれば、Traefik が直接アプリにルーティングする方がシンプルで正しいです。4 層スタックはその複雑さに見合う価値が特定のシナリオにあります:Nginx が必須の CloudPanel マネージド環境、フルページキャッシュが桁違いのスループット改善をもたらす CMS バックエンドサイト、または Traefik がすでに数十のコンテナのルーティング層となっているマルチサービスデプロイメントで、1 つに Varnish を追加することが新システムではなく追加コストとなる場合。
複雑さを長期的に管理可能にするのは、関心の分離です。Traefik 設定は Docker Compose ラベルに存在します。Varnish のロジックは compose スタックと一緒にバージョン管理された VCL ファイルに存在します。Nginx 設定は CloudPanel が管理します。これらのシステムのいずれも、他のシステムの領域に踏み込みません。予期しない動作が発生したとき、診断経路は明確です:X-Cache ヘッダーが Varnish がレスポンスを提供したかどうかを示し、Traefik のアクセスログがどのミドルウェアが実行されアップストリームが何を受け取ったかを記録し、Nginx のエラーログが PHP-FPM が何を報告したかを記録します。各層は独自の観測可能なシグナルを生成し、障害は層をまたいで静かに複合しません。
冗長に見えるものは、実際にはルーティング、キャッシング、アプリケーション提供、コンピュートの間の明確な境界です — 独立してスケールし、独立して障害を起こす 4 つの関心事。その境界は追加の設定コストに値します。