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-* を既定で付ける。振り分けの既定は random
  • health_uri で落ちた台を外せる。全台外れると 503、つなげないと 502
  • tls 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?