- Git
Caddyの使い方|Caddyfileでリバースプロキシと自動HTTPS
Caddy(キャディ)は、Go で書かれた Web サーバーです。設定ファイル Caddyfile にドメイン名を書くだけで、HTTPS の証明書を自動で取り、HTTP から HTTPS へのリダイレクトまで行います。 リバースプロキシ(受けたリクエストを裏のアプリに転送すること)は reverse_proxy の1行で書けます。
example.com {
reverse_proxy 127.0.0.1:3000
}
この3行で、次のことが起きます(ドキュメントの「Automatic HTTPS」による)。
| 動作 | 内容 |
|---|---|
| 証明書の取得・更新 | Let's Encrypt から取得。失敗したら ZeroSSL を使う |
| HTTP → HTTPS | 80番ポートへのアクセスを 443 へリダイレクト |
| HTTP/2・HTTP/3 | HTTPS では HTTP/1.1・HTTP/2・HTTP/3 で待ち受け |
| 転送ヘッダー | X-Forwarded-For・X-Forwarded-Proto・X-Forwarded-Host を付けて裏へ送る |
確認環境:Caddy v2.11.7(GitHub リリースの caddy_2.11.7_linux_amd64.tar.gz)を Ubuntu 24.04 で動かし、裏のアプリには Python の HTTP サーバーを2台立てました。自動 HTTPS は、インターネットに出ない内部 CA(tls internal)で証明書を発行させて確かめています。Let's Encrypt との通信と、ディレクティブの既定値は Caddy の公式ドキュメントで確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Caddyの使い方|Caddyfileでリバースプロキシと自動HTTPS」
Caddy のインストールと起動
公式のドキュメントでは、apt(Debian・Ubuntu)、Homebrew、Docker イメージ、GitHub リリースのバイナリなどの入れ方が案内されています。バイナリは1ファイルで動きます。
curl -LO https://github.com/caddyserver/caddy/releases/download/v2.11.7/caddy_2.11.7_linux_amd64.tar.gz
tar xzf caddy_2.11.7_linux_amd64.tar.gz caddy
./caddy version
# v2.11.7 h1:yj0Y4fYZGPkSvibBJ1sTWE33xC0fxztVyXEW5iIdUT4=
よく使うコマンドです。
| コマンド | すること |
|---|---|
caddy run |
前面で起動(カレントの Caddyfile を読む) |
caddy start / caddy stop |
背面で起動 / 停止 |
caddy reload |
止めずに設定を読み直す |
caddy validate --config Caddyfile |
設定を検証(Valid configuration) |
caddy fmt --overwrite |
Caddyfile の書式を整える |
caddy adapt --config Caddyfile --pretty |
Caddyfile を JSON に変換して表示 |
caddy reverse-proxy --from :8080 --to :3000 |
設定ファイルなしでプロキシを起動 |
caddy hash-password |
Basic 認証用のパスワードハッシュを作る |
ファイル名が Caddyfile 以外のときは、--adapter caddyfile が要ります。 付けずに caddy validate --config site.caddy とすると、JSON として読もうとして次のエラーになりました。
config is not valid JSON: invalid character 'l' looking for beginning of value, at offset 1; did you mean to use a config adapter (the --adapter flag)?
Caddy の本来の設定形式は JSON で、Caddyfile は JSON に変換して読み込まれます。下の Caddyfile(約40行)を caddy adapt すると、1,749 バイトの JSON になりました。
動かした Caddyfile(完成形)
リバースプロキシ・静的ファイル・ヘッダー・ログ・ヘルスチェック・内部 CA の HTTPS を1つにまとめたものです。ポートは確認用に 47080・47443 に変えています。本番で 80・443 を使うなら、先頭のグローバルオプションの http_port・https_port は消します。
{
admin localhost:47019
http_port 47080
https_port 47443
skip_install_trust
}
localhost {
tls internal
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
-Server
}
log {
output file /var/log/caddy/access.log
format json
}
handle /api/* {
reverse_proxy 127.0.0.1:47101 127.0.0.1:47102 {
lb_policy round_robin
health_uri /health
health_interval 2s
}
}
handle_path /static/* {
root * /srv/site
file_server
}
handle {
respond "Hello from Caddy" 200
}
}
Caddyfile の形は「サイトのアドレス { ディレクティブ }」です。いちばん上の { }(名前のないブロック)は、全体に効くグローバルオプションです。
| 部分 | 意味 |
|---|---|
admin localhost:47019 |
設定を受け付ける管理 API の待ち受け先(既定は localhost:2019) |
skip_install_trust |
内部 CA のルート証明書を OS の信頼ストアに入れない |
tls internal |
Let's Encrypt ではなく、Caddy 自身の CA で証明書を出す |
handle /api/* |
/api/ で始まるリクエストだけを処理するブロック |
handle_path /static/* |
handle と同じだが、/static を取り除いてから中を処理 |
handle(パスなし) |
ほかの handle に当たらなかったリクエスト |
自動 HTTPS を tls internal で確かめた
HTTP は 308 で HTTPS へ
HTTP のポートにアクセスすると、308 Permanent Redirect が返りました。
$ curl -sI http://localhost:47080/api/a
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://localhost/api/a
Server: Caddy
Location にポート番号は付きませんでした。80・443 で動かす本番では、このまま https://ドメイン/… に移ります。
内部 CA の証明書の中身
起動すると、ログに次の行が出て、すぐに証明書ができました。
"logger":"pki.ca.local","msg":"root certificate trust store installation disabled; unconfigured clients may show warnings"
"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"localhost","issuer":"local"
できた証明書を openssl で見ました。
| 証明書 | 発行者・名前 | 有効期間 |
|---|---|---|
ルート CA(pki/authorities/local/root.crt) |
Caddy Local Authority - 2026 ECC Root |
2026-10-13 〜 2036-08-21 |
サーバー証明書(certificates/local/localhost/) |
発行者 Caddy Local Authority - ECC Intermediate、SAN DNS:localhost |
12時間(06:46:54 〜 18:46:54 GMT) |
サーバー証明書は 12 時間で切れる短い証明書で、Caddy が自動で更新します。 ルート CA を信頼していないクライアントからは、curl で次のエラーになりました。
$ curl https://localhost:47443/
curl: (60) SSL certificate problem: unable to get local issuer certificate
$ curl --cacert root.crt https://localhost:47443/
Hello from Caddy
社内の検証環境で、ブラウザの警告を出さずに HTTPS を使いたいときは、root.crt を各端末に配ります。skip_install_trust を書かずに起動すると、Caddy はルート証明書を OS の信頼ストアに入れようとします(ドキュメントでは、パスワードを聞かれることがあると説明されています)。
証明書の置き場所
| OS | 置き場所(データディレクトリ) |
|---|---|
| Linux | $XDG_DATA_HOME/caddy(未設定なら ~/.local/share/caddy) |
| macOS | ~/Library/Application Support/Caddy |
| Windows | %AppData%\Caddy |
この中の certificates/ に証明書、pki/authorities/local/ に内部 CA が入ります。グローバルオプションの storage file_system <パス> で変えられます。また、caddy run は読み込んだ設定を ~/.config/caddy/autosave.json に保存していました(--resume で再利用するためのもの)。
本番(公開ドメイン)で証明書を取る条件
tls internal を外して example.com { … } と書くと、Let's Encrypt から取りに行きます。ドキュメントで挙げられている前提は次のとおりです。
- ドメインの A / AAAA レコードが、Caddy を動かすサーバーを指している
- 外から 80 番と 443 番に届く(HTTP-01・TLS-ALPN-01 の確認に使う)
- データディレクトリに書き込める
reverse_proxy の書き方
転送されるヘッダー
裏のアプリが受け取ったヘッダーです。Host は元のまま、X-Forwarded-* と Via が付きました。
{
"Host": "localhost:47443",
"User-Agent": "curl/8.5.0",
"Via": "2.0 Caddy",
"X-Forwarded-For": "127.0.0.1",
"X-Forwarded-Host": "localhost:47443",
"X-Forwarded-Proto": "https",
"Accept-Encoding": "gzip"
}
nginx では proxy_set_header を何行も書く部分が、Caddy では既定で入ります。ヘッダーを足す・消すときは header_up(裏へ送るヘッダー)と header_down(返すヘッダー)を使います。
reverse_proxy 127.0.0.1:3000 {
header_up X-Real-IP {remote_host}
header_up -Cookie
header_down -X-Powered-By
}
Caddy の前にさらにロードバランサーがあるときは、その IP 範囲を trusted_proxies に書きます。ドキュメントでは、書かないと、クライアント側から届いた X-Forwarded-* の値は信用されず無視されると説明されています。
振り分け(lb_policy)の一覧
裏に複数台を並べたときの選び方です。既定は random(ランダム)です。
| lb_policy | 選び方 |
|---|---|
random(既定) |
ランダム |
round_robin |
順番に |
weighted_round_robin 3 1 |
重み付きで順番に |
least_conn |
接続数がいちばん少ない台 |
first |
生きている中で先頭の台(予備機の構成に) |
ip_hash / client_ip_hash |
接続元 IP で同じ台に固定 |
uri_hash |
URI で同じ台に固定(キャッシュサーバー向け) |
header <名前> / query <名前> |
ヘッダー・クエリの値で固定 |
cookie [名前] |
Cookie で同じ台に固定(スティッキーセッション) |
round_robin にして4回続けて送ると、app2 → app1 → app2 → app1 と交互に振り分けられました。
ヘルスチェックで落ちた台を外す
health_uri を書くと、裏の台に定期的にリクエストを送り、応答を見ます(アクティブヘルスチェック)。ドキュメントの既定値は、間隔 30s・タイムアウト 5s・正常とみなすステータス 200 です。
| 書いたもの | 意味 | 既定値 |
|---|---|---|
health_uri /health |
チェックに使うパス | なし(書くと有効) |
health_interval 2s |
間隔 | 30s |
health_timeout 2s |
待つ時間 | 5s |
health_status 2xx |
正常とするステータス | 200 |
fail_duration 30s |
失敗した台を外しておく時間(パッシブ) | 0(オフ) |
max_fails 3 |
何回失敗したら外すか(パッシブ) | 1 |
lb_try_duration 5s |
失敗時に別の台で再試行する時間 | 0(再試行しない) |
app2 の /health が 503 を返すようにすると、ログに次の行が出て、すべて app1 に振り分けられました。
"logger":"http.handlers.reverse_proxy.health_checker.active","msg":"status code out of tolerances","status_code":503,"host":"127.0.0.1:47102"
/health を 200 に戻すと、"msg":"host is up" が出て、交互の振り分けに戻りました。
502 と 503 の違い
| 状況 | 返ったステータス | Caddy のログ |
|---|---|---|
| ヘルスチェックなしで、裏の台が止まっている | 502 | dial tcp 127.0.0.1:47109: connect: connection refused |
| ヘルスチェックで全台が外れている | 503 | no upstreams available |
502 は「つなぎに行って失敗した」、503 は「つなぎに行ける台がない」です。502 が続くときは、裏のアプリが起動しているか、ポート番号が合っているかを確かめます。
file_server で静的ファイルを返す
handle_path /static/* {
root * /srv/site
file_server
}
/static/index.html へのリクエストは、handle_path が /static を取り除くので、/srv/site/index.html が返りました。handle にすると /srv/site/static/index.html を探します。
HTTP/2 200
accept-ranges: bytes
content-type: text/html; charset=utf-8
etag: "dm3ibmcz1s5a-g"
last-modified: Tue, 13 Oct 2026 06:46:46 GMT
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
ETag・Last-Modified・Accept-Ranges は自動で付きました。 header で消した Server ヘッダーは、レスポンスにありませんでした。
SPA(React・Vue など)で、存在しないパスを index.html に回すときは try_files を使います。
example.com {
root * /var/www/html
try_files {path} /index.html
file_server
}
encode(圧縮)は 512 バイト未満では効かない
encode zstd gzip を書いても、16 バイトの Hello from Caddy は圧縮されませんでした。2,000 バイトのテキストは、content-encoding: gzip で 36 バイトになって返りました。ドキュメントでは、圧縮する最小サイズ(minimum_length)の既定は 512 バイトです。
log でアクセスログを出す
log ディレクティブを書いたサイトだけ、アクセスログが出ます。 JSON 形式で、1リクエスト1行です(読みやすく整形しています)。
{
"level": "info",
"logger": "http.log.access.log0",
"msg": "handled request",
"request": {
"remote_ip": "127.0.0.1",
"client_ip": "127.0.0.1",
"proto": "HTTP/2.0",
"method": "GET",
"host": "localhost:47443",
"uri": "/api/x",
"tls": {"version": 772, "proto": "h2", "server_name": "localhost"}
},
"duration": 0.001418274,
"size": 270,
"status": 200
}
output file のログは、ドキュメントの既定では 100MiB でローテートし、10ファイル・90日分を残します。
log {
output file /var/log/caddy/access.log {
roll_size 50MiB
roll_keep 5
roll_keep_for 720h
}
format json
}
nginx の設定との対応表
| nginx | Caddyfile |
|---|---|
server { server_name example.com; } |
example.com { } |
listen 443 ssl; + ssl_certificate |
書かない(自動 HTTPS) |
return 301 https://$host$request_uri; |
書かない(自動で 308) |
location /api/ { proxy_pass http://app; } |
handle /api/* { reverse_proxy app:3000 } |
proxy_set_header X-Forwarded-For … |
書かない(既定で付く) |
upstream app { server a; server b; } |
reverse_proxy a:3000 b:3000 |
root /var/www; + location / { } |
root * /var/www + file_server |
try_files $uri /index.html; |
try_files {path} /index.html |
add_header X-Frame-Options DENY; |
header X-Frame-Options DENY |
gzip on; |
encode gzip |
access_log /var/log/…; |
log { output file /var/log/… } |
auth_basic + htpasswd |
basic_auth { ユーザー ハッシュ } |
rewrite ^/old$ /new permanent; |
redir /old /new 301 |
client_max_body_size 10m; |
request_body { max_size 10MB } |
nginx -t |
caddy validate |
nginx -s reload |
caddy reload |
上の Caddyfile 側の書き方は、caddy adapt で JSON に変換でき、caddy fmt で書式も崩れていないことを確かめました。nginx 側のリバースプロキシの詳しい書き方は、関連記事「Nginxリバースプロキシ設定|proxy_passの書き方と502/504対処」で扱っています。
Caddy と nginx の違い
| 項目 | Caddy | nginx |
|---|---|---|
| HTTPS 証明書 | 自動で取得・更新 | certbot などを別に用意 |
| 設定の形式 | Caddyfile(中身は JSON) | nginx.conf |
| 設定の変更 | caddy reload か管理 API(HTTP で JSON を送る) |
ファイルを書き換えて reload |
| HTTP/3 | HTTPS で既定で有効 | listen … quic の設定が要る |
| 拡張 | Go のモジュールを入れて再ビルド(xcaddy) |
動的モジュール・Lua など |
nginx で証明書の自動更新を組む方法は、関連記事「Let's Encrypt certbot自動更新|45日化とnginx設定手順」で扱っています。
ディレクティブの実行順に注意
Caddyfile のディレクティブは、書いた順ではなく、Caddy が決めた順番に並べ替えて実行されます。 例えば respond と reverse_proxy を同じブロックに並べると、どちらが先かはドキュメントの「Directive order」の表で決まります。順番をはっきりさせたいときは、handle で分けるか、書いた順に実行する route を使います。
| ブロック | 動き |
|---|---|
handle |
当たった1つだけを実行(互いに排他) |
handle_path |
handle と同じで、パスの先頭を取り除く |
route |
中のディレクティブを書いた順に実行 |
よくあるエラーと直し方
| エラー・状況 | 原因 | 直し方 |
|---|---|---|
unrecognized directive: reverse_proxi |
ディレクティブ名の打ち間違い | 名前を直す |
syntax error: unexpected token '127.0.0.1:1', expecting '}' |
} の閉じ忘れ |
ブロックを閉じる |
config is not valid JSON … did you mean to use a config adapter |
ファイル名が Caddyfile ではない |
--adapter caddyfile を付ける |
Caddyfile input is not formatted; run 'caddy fmt --overwrite'(警告) |
インデントが Tab でない等 | caddy fmt --overwrite |
curl: (60) SSL certificate problem: unable to get local issuer certificate |
tls internal のルート CA を信頼していない |
root.crt を配る・信頼させる |
502 と connection refused |
裏のアプリが止まっている・ポート違い | 裏のアプリとポートを確かめる |
503 と no upstreams available |
ヘルスチェックで全台が外れた | health_uri の応答を確かめる |
上の4つの文面は、caddy validate と curl で再現したものです。
よくある質問
Q. 80 番・443 番以外のポートで HTTPS にできますか。
A. できます。今回は https_port 47443 で HTTPS を受けました。ただし、公開ドメインで Let's Encrypt から取るときは、ドキュメントにあるとおり、確認のために外から 80・443 に届く必要があります(DNS-01 チャレンジのプラグインを使う方法もあります)。
Q. 設定を変えたあと、止めずに反映できますか。
A. caddy reload --config Caddyfile で反映できました。管理 API(今回は localhost:47019)に新しい設定を送る仕組みです。
Q. WebSocket は別の設定が要りますか。
A. ドキュメントでは、reverse_proxy は WebSocket の接続(HTTP の Upgrade)にも対応しているとされています。nginx のように Upgrade・Connection ヘッダーを書き足す設定はありません。なお、設定を reload すると既定では WebSocket の接続が切られます。stream_close_delay で切るまでの時間を延ばせます。
Q. 証明書の中身を詳しく見るには?
A. データディレクトリの .crt を openssl x509 -noout -issuer -subject -dates -ext subjectAltName -in 〜.crt で見ました。openssl の使い方は、関連記事「opensslコマンドの使い方|目的別一覧と実行結果・エラー対処【3系】」で扱っています。
まとめ
- Caddyfile は「アドレス { ディレクティブ }」。ドメインを書くと 証明書の取得・更新と HTTP→HTTPS(308)が自動
reverse_proxyはX-Forwarded-*を既定で付ける。振り分けの既定はrandomhealth_uriで落ちた台を外せる。全台外れると 503、つなげないと 502tls internalの証明書は 12時間で自動更新。ルート CA を配ると警告が消える- ディレクティブは書いた順ではなく決まった順で動く。
handle・routeで分ける
参考・出典
確認日はいずれも 2026年10月13日です。
- Caddy ドキュメント「The Caddyfile」:https://caddyserver.com/docs/caddyfile
- Caddy ドキュメント「Automatic HTTPS」:https://caddyserver.com/docs/automatic-https
- Caddy ドキュメント「reverse_proxy」:https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
- Caddy ドキュメント「encode」:https://caddyserver.com/docs/caddyfile/directives/encode
- Caddy ドキュメント「log」:https://caddyserver.com/docs/caddyfile/directives/log
- Caddy ドキュメント「Caddyfile Directives」(Directive order):https://caddyserver.com/docs/caddyfile/directives
- Caddy ドキュメント「Conventions」(データディレクトリ):https://caddyserver.com/docs/conventions
- caddyserver/caddy リリース v2.11.7:https://github.com/caddyserver/caddy/releases/tag/v2.11.7
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Caddyの使い方|Caddyfileでリバースプロキシと自動HTTPS」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。