NginxとCaddyとTraefikの選び方
1台のVPSと1つの公開IPで4つのアプリを運用するなら、TLS証明書、アプリ追加時の設定量、WebSocket、Dockerルーティングの違いから最適なプロキシを選べます。
Nginx vs Caddy vs Traefik: 要点
Nginx、Caddy、Traefik は、いずれもリバースプロキシとして同じ役割を担います。443 番ポートで待ち受け、各リクエストのホスト名を読み取り、VPS 上の適切なサービスへ転送します。3 つのいずれを使っても、1 つの公開 IP アドレスの背後で 4 つのセルフホストアプリを運用できます。いずれも十分に高速なため、速度上のボトルネックになるのはアプリの側です。違いは、TLS(transport layer security)証明書を取得する方法と、アプリを追加するたびに必要となる設定量です。もう 1 つの違いは、一般的なチュートリアルでは扱われない機能が必要になったときに現れます。
HTTPS の処理を任せたい場合や、通常の Web アプリを運用する場合は Caddy を選びます。すべてを Docker Compose で実行し、数週間ごとに新しいサービスを追加する場合は Traefik を選びます。すでに Nginx を運用している場合、またはレスポンスキャッシュ、クライアント証明書、raw TCP 転送、大規模な既存設定を必要とし、それを作り直したくない場合は Nginx を選びます。
各方式で TLS 証明書を取得する方法
この軸で大半の判断が決まるため、ここから確認します。3 つの方式は、最終的に同じ認証局から同じ証明書を取得します。そこに至るまでの作業は同じではありません。
Caddy はホスト名を指定すると証明書を要求します。 app.example.com をサイトアドレスとして記述すると、Caddy は ACME(automatic certificate management environment)を使用して Let's Encrypt に証明書を要求します。取得に失敗すると ZeroSSL にフォールバックし、port 80 で HTTP から HTTPS へのリダイレクトを提供し、自動的に更新します。別のツールや確認用の timer は必要ありません。証明書は caddy ユーザーのデータディレクトリに保存されます。パッケージでインストールした場合は /var/lib/caddy/.local/share/caddy です。このパスをバックアップ対象に追加するか、再構築後に新しい証明書を発行する運用にします。公開されていないホスト名の場合、tls internal により Caddy 自身のローカル認証局で署名します。これは Ubuntu で自己署名証明書を作成する場合と同じ状態ですが、更新は Caddy が処理します。
Nginx には ACME クライアントがありません。 Certbot が証明書を取得し、その --nginx プラグインが server block を書き換えて、443 の listener とリダイレクトを追加します。更新はパッケージがインストールする systemd timer で実行されるため、確認すべき要素が 2 つあります。systemctl list-timers | grep certbot で timer の存在を確認し、sudo certbot renew --dry-run で更新処理が引き続き機能することを検証します。手順の詳細は Nginx を使用した Ubuntu 24.04 の Certbot にあります。登録したいサブドメインが多い場合は、同じツールで DNS-01 challenge による wildcard certificate も取得できます。
Traefik には独自の ACME クライアントが組み込まれています。 static configuration で 1 つの certificate resolver を設定すると、すべての router で使用できます。account key と証明書を含むすべての状態は、単一の acme.json ファイルに保存されます。Traefik は、所有者以外がこのファイルを読み取れる場合、そのファイルを使用しません。resolver を無効にする前に、その理由を表示します。
The ACME resolver "le" is skipped from the resolvers list because: unable to get ACME account: permissions 660 for /letsencrypt/acme.json are too open, please use 600ディレクトリを mount し、Traefik 自身にファイルを作成させます。最初に touch でファイルを作成すると、umask が適用されます。多くの環境では、これによりこのエラーを回避できます。
3 方式すべてに共通する点が 1 つあります。HTTP-01 challenge では、インターネットから port 80 に到達できる必要があります。認証局が port 80 に接続して検証するためです。443 だけを開放すると、DNS の問題に見える形で証明書の発行に失敗します。
3 つの設定で同じ 2 アプリへのルーティングを構成する
目的は次のとおりです。app.example.com は 127.0.0.1:8080 のサービスへ、files.example.com は 127.0.0.1:8081 のサービスへ、どちらも HTTPS 経由で転送します。各プロキシでの全体設定を示すため、記述量の違いを実際に確認できます。
Nginx
# /etc/nginx/sites-available/app.example.com
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}次にリンクを作成し、設定をテストしてから reload し、証明書を追加します。
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d app.example.comnginx -t で syntax is ok と test is successful を出力する確認は、reload の前に毎回実行します。2 つ目のアプリも同じブロックを使い、ホスト名とポートだけを変更します。proxy_set_header の行は飾りではありません。proxy_pass がアドレスを指定すると、nginx はデフォルトで Host: 127.0.0.1:8080 を upstream に送信します。そのため、Host ヘッダーから絶対 URL を生成するアプリでは、ユーザーを localhost に誘導してしまいます。
Caddy
app.example.com {
reverse_proxy 127.0.0.1:8080
}
files.example.com {
reverse_proxy 127.0.0.1:8081
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyファイル全体はこれだけです。reverse_proxy が X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host を自動的に設定します。また、デフォルトではクライアントがそれらのヘッダーに送信した値を無視するため、リクエストからバックエンドに送信元を偽装することはできません。証明書、ポート 80 からのリダイレクト、更新は、2 つのサイトアドレスから自動的に設定されます。ファイル内で、それらを個別に要求する設定はありません。
Traefik
Traefik でルーティングを開始するには、最初に静的設定が必要です。Compose サービスとして、2026 年 8 月時点で現行の image tag を使用する場合は次のようになります。
services:
traefik:
image: traefik:v3.7
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt次に、各アプリケーションがそれぞれの compose ファイル内の labels で、独自のルーティングを定義します。
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=8080"loadbalancer.server.port はコンテナ内部のポートであり、公開ポートではありません。Traefik は共有 Docker network 経由でコンテナに接続するためです。アプリには ports: 行さえ必要ありません。これが本当の利点です。公開するのは Traefik だけです。共有 network とリダイレクト middleware を含む完全な構成は、Traefik と Docker Compose で複数アプリをルーティングする にあります。
追加するアプリごとに必要な設定量
The data behind this chart
[
{
"tool": "Nginx",
"proxy_setup_lines": 0,
"lines_per_app": 11
},
{
"tool": "Caddy",
"proxy_setup_lines": 0,
"lines_per_app": 3
},
{
"tool": "Traefik",
"proxy_setup_lines": 17,
"lines_per_app": 5
}
]上記のブロックを基準に数えます。Nginx の server block は空行を除いて 11 行で、ホスト名ごとに同じ設定を記述します。Caddy の site block は 3 行です。Traefik は、最初のリクエストを処理する前に静的設定として 17 行が必要で、その後はアプリごとに 5 個の label を指定します。
単純な勝敗ではなく、トレードオフを見てください。Traefik は最初のアプリを追加するまでのコストが最も高く、その後のアプリごとのコストが最も低くなります。2つの合計は、3つ目あたりのサイトでほぼ同じになります。それより少ない場合は、不要だった静的設定がオーバーヘッドになります。それを超えると label 側が優位になり、その差は広がり続けます。ルーティング設定が、ルーティング対象のサービスの隣に置かれるためです。サービスを削除すれば、そのルートも削除されます。これは、存在しなくなって数か月たつアプリの古い server block が残るという、中央集約型の設定ファイルが苦手とする問題を解消します。
行数だけで見ると、Nginx は有利に見えます。しかし、各ブロックには symlink の作成、nginx -t、reload、certbot の実行が必要です。一方、Caddy の編集では reload が1回必要なだけで、Traefik の編集ではコマンド自体が不要です。3つとも、稼働中の接続を切断せずに reload できます。違いは、午前1時に覚えておく必要がある個別の手順の数です。
コンテナを把握できるのはどれですか?
Traefik は Docker socket を監視し、コンテナの起動と停止に合わせて、コンテナの labels から routers を構成します。ここで、これを自動的に行うものはほかにありません。新しいコンテナが追加されると、Nginx と Caddy はどちらも設定を編集して reload する必要があります。また、到達可能なアドレスも必要です。loopback 上で公開した port、または proxy を接続した共有 Docker network のいずれかです。
この機能には代償があります。ここは明確にしておく必要があります。Traefik は /var/run/docker.sock を読み取ります。この socket に接続できるユーザーは、host filesystem をコンテナ内に mount した状態でコンテナを起動できます。これは host 上の root 権限に相当します。read only で mount すればリスクは下がりますが、なくなるわけではありません。脅威モデル上これが問題になる場合は、Traefik に必要なコンテナ一覧の endpoints だけを公開する socket proxy を間に配置してください。
Caddy は community plugin により labels を使った discovery に対応できます。ただし、Caddy plugins は binary に組み込まれるため、xcaddy を使って custom binary または custom image を build する必要があります。その build と更新は自分で管理することになります。サービスが 3 つか 4 つ程度なら、Caddyfile を編集するほうが手間は少なくて済みます。
WebSocket とストリーミングで壊れるものと、その理由
対応が必要なのは Nginx です。WebSocket 接続は Upgrade: websocket を含む HTTP リクエストとして開始されます。nginx は、明示的に指定しない限り hop-by-hop ヘッダーを upstream に渡しません。
# /etc/nginx/conf.d/upgrade-map.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}次に、location ブロック内には、次の 3 行をすべて記述する必要があります。
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;これらを省略すると、ブラウザーのコンソールには WebSocket connection to 'wss://app.example.com/ws' failed と表示され、バックエンドのログには通常の GET と記録されます。map が必要なのは、ハードコードした Connection: upgrade をすべてのリクエストに送信すると、通常のリクエストにも適用され、本来は close とすべきものまで同じ扱いになるためです。
Nginx のデフォルト設定には、さらに 2 つ注意点があります。proxy_read_timeout は 60 秒で、アップグレード後のトンネルにも適用されます。そのため、1 分間通信がない WebSocket はプロキシによって切断されます。また、対象 location に proxy_buffering off; を設定しないと、server-sent events は遅れて届くか、まとめて届きます。nginx がレスポンスをバッファーに保持するため、ページ側がイベントを待ち続けることになるためです。
Caddy はディレクティブを追加しなくてもアップグレードを実行し、接続を双方向トンネルに切り替えます。また、レスポンスが text/event-stream であるか、長さが不明な場合は直ちにフラッシュするため、そのままストリーミングを利用できます。Traefik はアップグレードを通過させ、独自の buffering middleware を追加しない限りレスポンスをバッファーしません。サービスにチャット、Web ターミナル、ログの追跡表示、ライブダッシュボードが含まれる場合、記述してデバッグする設定量に大きな違いが生じます。
WebSocket と SSE を含む Nginx の完全な server ブロック
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name app.example.com;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}map は http コンテキストに配置し、server 内には記述しません。そのため、/etc/nginx/conf.d/ 配下の独立したファイルに記述してください。proxy_buffering はストリーミングを行う location でのみ無効にします。通常のレスポンスでは、バッファリングによって nginx がバックエンドの worker を早期に解放できるためです。Certbot は実行時にこのブロックを書き換えるため、実行後にファイルを再度確認してください。
特殊な要件が必要になった場合
ここで Nginx の行数の多さが意味を持ちます。
- クライアント証明書(mTLS(相互 TLS)とも呼ばれます)では、クライアントも証明書を提示する必要があります。Nginx では、server ブロックに
ssl_client_certificate /etc/ssl/ca.pem;とssl_verify_client on;を記述します。Caddy では、tlsの内部にclient_authブロックを記述します。Traefik のラベルだけでは、これをまったく表現できません。file provider で TLS option を定義し、traefik.http.routers.app.tls.options=mtls@fileで router に指定します。ラベルだけですべてを設定するモデルも、この要件が初めて発生した時点で例外が必要になります。 - 大容量のアップロードでは、Nginx がリクエストボディをデフォルトで 1 MB に制限します。より大きいアップロードは
413 Request Entity Too Largeを返し、error log にはclient intended to send too large bodyと記録されます。client_max_body_sizeを増やしてください。Caddy と Traefik にはデフォルトでボディサイズの制限がないため、リクエストはアプリに到達し、アプリ独自の制限が適用されます。 - レスポンスのキャッシュでは、Nginx に
proxy_cacheがあり、成熟した機能として利用できます。Caddy では plugin を組み込んでコンパイルする必要があります。Traefik の open source build には HTTP cache がまったくないため、すべての proxy がキャッシュすると考えている利用者は注意が必要です。 - データベースのポートやゲームサーバーなどの raw TCP または UDP では、Nginx に
streammodule があります。Traefik には、それぞれ独自の entrypoint で TCP router と UDP router があります。Caddy では別の plugin が必要になるため、さらに custom build が必要です。 - proxy の背後にすでに Web server がある場合、そのサービスが従来型の PHP application であれば、Ubuntu 24.04 の LAMP stack にすでに Apache が含まれています。さらにその前段に proxy を置くと、header を設定する場所が 2 か所になり、URL を書き換える場所も 2 か所になります。どちらで TLS を終端するかを決め、もう一方は loopback に bind した plain HTTP のままにしてください。
この選択に続いて起きるファイアウォールの落とし穴
リバースプロキシを使う目的は、80 と 443 だけを開放することです。Docker はこの構成を暗黙に崩します。-p 8080:80 でポートを公開すると、nat テーブルに DNAT ルールが追加されます。このルールは ufw が管理する INPUT ルールより前に評価されるため、ufw deny 8080 ではブロックできません。その結果、アプリケーションは、慎重に設定したプロキシと並んでパブリックインターネットから直接到達できる状態になります。公開ポートは 127.0.0.1:8080:80 で loopback にバインドしてください。または ports: を完全に削除し、Docker network 経由でプロキシからコンテナへ接続させてください。上記の Traefik の例はこの方法です。仕組みと対処方法については、Docker の公開ポートが ufw をバイパスする理由を参照してください。
VPS 外部のマシンからテストしてください。VPS 自身で実行した確認は必ず成功するためです。
curl --max-time 5 http://your.server.address:8080Connection refused またはタイムアウトになれば、期待どおりです。HTTP レスポンスが返る場合、そのアプリケーションにはプロキシを経由せず到達できます。上記で設定した内容はすべて形だけになります。
どのプロキシを選ぶべきか
主に静的サイトを運用し、アプリを 1 つか 2 つ追加する場合: Caddy。 自動 HTTPS により、繰り返し発生する作業の中で最も大きなものをなくせます。設定は 1 画面で読める程度に短く、静的サイトは同じ site ブロック内に root 行と file_server 行を記述するだけです。代わりに、問題が起きたときにそのまま使える回答の蓄積は少なくなります。
追加するサービスが増え続ける docker-compose のホームラボ: Traefik。 3 つ目のサービスを超えると、中央のファイルを編集するよりラベルを使うほうが手間が少なく、サービスを削除すればそのルートも削除されます。entrypoint、router、service、middleware はすべて新しい用語なので、初回の構築には半日ほど見込んでください。ラベルのタイプミスは通常、起動失敗ではなく Traefik からの 404 として現れます。そのため、アプリが壊れていると判断する前に、構文解析エラーについて docker logs traefik を確認してください。
既存の Nginx 設定がある場合、または上記の一覧にある要件がある場合: Nginx。 レスポンスキャッシュやクライアント証明書にはすでに対応方法があり、ほとんどすべてのサードパーティー製ガイドが Nginx を前提にしています。一方、証明書と websocket のサポートは、自動的に利用できる機能ではなく、自分で設定する必要があります。
どれを選んでも、1 つのルールが適用されます。公開インターフェースで待ち受けるプロセスは正確に 1 つだけにし、それ以外は loopback またはプライベートな Docker ネットワークで待ち受けさせます。
FAQ
1 台の VPS で少数の Docker アプリを運用する場合、どのリバースプロキシが最適ですか?
追加するサービスが 3 つか 4 つ程度で、追加頻度も高くない場合は、Traefik が適しています。各アプリが独自のルーティングラベルを持つため、中央の設定ファイルを編集する必要がありません。サービスの構成が安定していて、HTTPS の管理負担を減らしたい場合は、Caddy のほうが習得しやすく、設定を壊しにくいです。すでに Nginx に慣れている場合や、レスポンスキャッシュや単純な TCP リスナーなど、他の 2 つにない機能が必要な場合は Nginx を選びます。
Caddy では本当に証明書の設定が不要ですか?
通常の構成であれば、不要です。公開ホスト名をサイトアドレスとして指定するだけで設定は完了します。Caddy は ACME 経由で証明書を要求し、port 80 からのリダイレクトを提供し、期限切れ前に更新します。ただし、2 つの条件を満たす必要があります。HTTP-01 challenge のために port 80 へインターネットから到達できることと、証明書認証局が名前を解決して接続できるよう、ホスト名の DNS A または AAAA レコードがあらかじめ VPS を指していることです。
同じ VPS で Nginx と Traefik を実行できますか?
同じポートでは実行できません。後から起動したほうが bind に失敗し、nginx は bind() to 0.0.0.0:443 failed (98: Address already in use) と表示します。一方、Traefik は同様の bind エラーをログに出力して終了します。1 つのプロキシを 80 と 443 で実行し、それ以外のサービスはその背後に配置してください。移行中の場合は、ホスト名を 1 つずつ移行します。最後のサイトの移行が完了するまで、前段のプロキシから loopback port 上の旧プロキシへ転送します。
Nginx の背後で WebSocket が 60 秒後に切断されるのはなぜですか?
proxy_read_timeout のデフォルト値は 60 秒です。upgrade 完了後のトンネルにも適用されるため、1 分間通信がない接続はアプリではなくプロキシによって閉じられます。その location で proxy_read_timeout 3600s; を設定して値を大きくするか、アプリケーションから 30 秒ごとに ping frame を送信してください。Caddy と Traefik は、1 分間のタイマーによって idle 状態の upgrade 済み接続を閉じません。そのため、同じアプリでも Nginx の背後では不安定に見え、Caddy や Traefik の背後では安定して見えることがあります。