【PR】この記事にはA8.netのアフィリエイト広告を含みます。
先に結論:Caddy 2.10.2のreverse_proxyで平文HTTP upstreamをhttps://指定すると、XServer VPS上の実測では5/5回が502になり、ログへtls: first record does not look like a TLS handshakeが残りました。自己署名HTTPSはschemeを正しくしてもCA未信頼なら5/5回が502です。HTTP/HTTPS、証明書trust、Host headerを順に確認すれば切り分けられるため、判定は切り分けについて解決です。
- Caddyの502はHTTPとHTTPSの取り違えで起きる?
- Caddy公式ではupstream schemeをどう扱う?
- XServer VPSでどんな実験をした?
- HTTP・HTTPSの6設定はどうなった?
- HTTP upstreamをHTTPS指定すると何が記録された?
- HTTPS upstreamをHTTP指定すると必ず502になる?
- schemeをHTTPSへ直したのに502のままだった理由は?
- 自己署名HTTPS upstreamはどう直す?
- 200なのにbodyが空だったのはなぜ?
- Caddy 502を調べる順番は?
- 実験中にどんな問題が起きた?
- CPU・RAM・ストレージへの影響は?
- 本番サービスと後片付けは?
- 初心者が迷いそうなポイントは?
- XServer VPSでこの悩みはどこまで解決した?
- よくある質問
- 最新条件の確認先
Caddyの502はHTTPとHTTPSの取り違えで起きる?
起きます。ただし、取り違える向きによって同じstatusになるとは限りません。今回、HTTP serverへTLSで接続すると502、HTTPS serverへ平文HTTPで接続すると400になりました。
悩みの起点にしたReddit「Authentik behind Caddy Docker Proxy」では、Authentikの9443番へproxyした際にClient sent an HTTP request to an HTTPS serverが出て、HTTP portへ変えると502になったと報告されています。投稿者はupstreamへhttpsを明示して解決したと追記しています。
Caddy公式ではupstream schemeをどう扱う?
Caddy公式のreverse_proxy docsによると、schemeを省略したupstreamは既定で平文HTTPです。https://を付けるとHTTP transportのTLSが有効になります。port番号だけでHTTPSとは推測されません。
# 平文HTTP upstream
reverse_proxy http://app:8080
# HTTPS upstream
reverse_proxy https://app:8443
つまり、外側がHTTPSかどうかと、Caddyからappまでの内側通信がHTTPかHTTPSかは別設定です。browser→CaddyがHTTPSでも、Caddy→appはHTTPという構成は普通にあります。
XServer VPSでどんな実験をした?
| 項目 | 実測条件 |
|---|---|
| VPS | XServer VPS、4 vCPU、4GB RAM、150GB |
| OS | Ubuntu 26.04 |
| Caddy | 2.10.2-alpine |
| upstream | 平文HTTP :8080、内部CAのHTTPS :8443 |
| proxy | 一時Caddy :8081 |
| network | Docker internal network、公開portなし |
| 試行 | 6設定×5回、計30 request |
| 上限 | 各Caddy 0.25 CPU、128MiB RAM、PID 96 |
本番Caddyfileや公開portは変更していません。HTTP upstream、HTTPS upstream、proxy、clientは一時network内だけで起動し、status、body、proxy log、cgroup値を採取しました。
[PR]
VPSでDockerとCaddyを試すなら『XServer VPS』![]()
HTTP・HTTPSの6設定はどうなった?
| 設定 | 5回のstatus | 中央値 | body |
|---|---|---|---|
| HTTP→HTTP | 200×5 | 358.174ms | upstream-http-ok |
| HTTPをHTTPS扱い | 502×5 | 349.647ms | 空 |
| HTTPSをHTTP扱い | 400×5 | 305.802ms | 空 |
| HTTPS、CA未信頼 | 502×5 | 252.519ms | 空 |
| HTTPS、検証skip、元Host | 200×5 | 296.964ms | 空 |
| HTTPS、検証skip、upstream Host | 200×5 | 266.775ms | upstream-https-ok |
30/30 requestがcaseごとの期待statusと一致しました。ただし「200だから正常」とは限りません。5番目はstatus 200でもbodyが0 byteでした。health checkはstatusだけでなく、期待するJSON fieldや文字列まで検証する必要があります。
HTTP upstreamをHTTPS指定すると何が記録された?
proxyを次のように設定しました。
reverse_proxy https://llmo-exp-30-upstream-http:8080
平文HTTP serverへTLS ClientHelloを送ったため、5回すべて502になりました。Caddyのapplication logには各requestで次のmessageとstatusが残りました。
tls: first record does not look like a TLS handshake
status: 502
err_trace: reverseproxy.statusError
接続拒否やDNS errorとは違い、TCP接続先は存在するもののprotocolが違う状態です。container名とportだけでなく、app側がそのportで実際にHTTPとHTTPSのどちらを話すかを確認します。
HTTPS upstreamをHTTP指定すると必ず502になる?
今回のCaddy同士では502ではなく400でした。http://...:8443で平文GETを送ると、TLS listenerがHTTP requestを受けて400 Bad Requestを返し、proxyがそのstatusをclientへ渡しました。
upstream実装が接続を切る場合は502になることもあります。statusだけで原因を固定せず、Caddy log、upstream log、scheme、portの4点を合わせて判断します。
schemeをHTTPSへ直したのに502のままだった理由は?
自己署名HTTPS upstreamをhttps://で指定すると、protocolは一致しましたがCAが未信頼でした。5/5回が502で、次の実ログを取得しました。
tls: failed to verify certificate:
x509: certificate signed by unknown authority
schemeを直すことと、証明書を信頼できることは別条件です。productionでは公開CAの証明書、または管理しているprivate CAをCaddyへ明示的に信頼させます。
自己署名HTTPS upstreamはどう直す?
Caddy公式docsには、tls_trust_pool fileでupstreamのCAを信頼し、必要ならtls_server_nameを証明書のhostnameへ合わせる例があります。
reverse_proxy https://app:8443 {
transport http {
tls_trust_pool file /run/certs/upstream-ca.pem
tls_server_name app.internal
}
}
今回のtls_insecure_skip_verifyは、CA検証が原因かを判定する診断だけに使いました。公式docsもproductionでは使わないよう明記しています。検証を止めたまま運用すると、中間者攻撃や接続先偽装を検出できません。
200なのにbodyが空だったのはなぜ?
Caddy 2.10.2では受信したHostがHTTPS upstreamへそのまま渡りました。TLS handshakeを通過しても、upstream Caddyのsite addressとHostが一致せず、期待routeに入らなかったため200・0 byteになりました。
reverse_proxy https://app:8443 {
header_up Host {upstream_hostport}
transport http {
tls_insecure_skip_verify
}
}
header_up Host {upstream_hostport}を加えると、upstream-https-okを5/5回取得できました。Caddy公式docsでは、HTTPS upstreamのHost調整はv2.11.0以降に自動化されています。今回のXServer VPSは2.10.2だったため、version差も問題の一部でした。
Caddy 502を調べる順番は?
- Docker network内からupstream名とportへ到達できるか確認する。
- upstreamがHTTPかHTTPSか、app公式設定または直接requestで確認する。
reverse_proxyへ正しいhttp://またはhttps://を明示する。- HTTPSならcertificate chain、CA trust、SNI名を確認する。
- Caddy 2.10以前ではHost headerがupstream routeと一致するか確認する。
- statusだけでなくresponse bodyとCaddy error messageを記録する。
first record does not look like a TLS handshakeならHTTPS→HTTP serverの可能性、x509: certificate signed by unknown authorityならschemeは合っているがtrust設定が不足している可能性が高いです。
実験中にどんな問題が起きた?
初回は隔離性を上げるため--cap-drop=ALLとno-new-privilegesを指定しましたが、Caddyが起動前に停止しました。さらにcollectorがstdoutしか読んでおらず、最初は停止理由が空に見えました。
stderrも保存するよう直すと、exec /usr/bin/caddy: operation not permittedを確認できました。Caddy binaryのfile capabilityとcontainerのbounding setが合っていなかったため、公開portを作らずNET_BIND_SERVICEだけを戻しました。再実験は正常に完走しました。
CPU・RAM・ストレージへの影響は?
- proxy Caddy peak RAM:11.85〜12.38MiB
- HTTP upstream peak RAM:13.15MiB
- HTTPS upstream peak RAM:14.15MiB
- proxy CPU:caseごとに0.076074〜0.083502秒
- host MemAvailable:約3277.81MiBから3242.26MiB
- root disk差:290,816 byte増
- 成功run総時間:14.560545秒
request時間には一時client containerの起動を含みます。upstreamだけの純network latencyではなく、設定差分を同条件で再現するための値です。
本番サービスと後片付けは?
公開/health/readyは開始前、実験中、終了後の全3回でHTTP 200でした。database=ok、artifact_storage=okも維持しています。
本番Caddy、FastAPI、PostgreSQLは前後ともrestart 0でした。一時container 3個とinternal networkを削除し、dangling volumeは前後0件、公開portも0件です。
初心者が迷いそうなポイントは?
- browser→CaddyのHTTPSと、Caddy→appのprotocolは別。
- 9443や8443というport名だけではCaddyはHTTPSを推測しない。
- schemeを直しても、自己署名CAが未信頼なら502になる。
tls_insecure_skip_verifyは恒久対策ではない。- 200でもbodyが期待値と違えば正常とはいえない。
- Caddy 2.10と2.11以降ではHTTPS upstreamのHost処理が異なる。
docker logsはstdoutとstderrの両方を確認する。
XServer VPSでこの悩みはどこまで解決した?
切り分けについて解決です。HTTPをHTTPS扱いした502、HTTPSをHTTP扱いした400、CA未信頼の502、Host不一致の200・空bodyを別々に再現できました。正しいHTTP設定と、診断用にtrust・Host条件をそろえたHTTPS設定は各5/5回200でした。
production用のprivate CA trust pool、Authentik本体、mTLS、複数upstreamのload balancingは未検証です。次の実装ではtls_trust_pool fileを使い、certificate検証を有効にしたままHTTPS upstreamへ接続します。
よくある質問
Q. Caddyのreverse_proxyはscheme省略時にHTTPSになりますか?
A. なりません。既定は平文HTTPです。HTTPS upstreamにはhttps://を明示します。
Q. 502ならHTTP・HTTPS取り違えで確定ですか?
A. 確定ではありません。DNS、接続拒否、timeout、certificate検証でも502になります。error logのmessageで区別します。
Q. first record does not look like a TLS handshakeとは何ですか?
A. CaddyがTLSとして接続した先が平文HTTPなど、TLS以外の最初のbyteを返した可能性が高いという意味です。
Q. 自己署名証明書はtls_insecure_skip_verifyでよいですか?
A. 診断用途に限定します。本番ではCA certificateをtls_trust_pool fileで信頼させます。
Q. status 200なら修正完了ですか?
A. いいえ。今回もHost不一致で200・空bodyが出ました。期待body、JSON field、application-level healthまで確認します。
実験日:2026年9月2日。数値は筆者が契約中のXServer VPS 4GB環境における各5回の実測です。Caddy version、upstream、certificate、Docker networkにより結果は変わります。


コメント