Caddy で自宅サーバーをHTTPS化する — nginx より楽すぎて戻れなくなった話
先週、自宅の Raspberry Pi 5 に動かしているダッシュボードを家族にも見せたくなった。条件は二つだけ。HTTPS にすること。設定を最小限にすること。
nginx を引っ張り出して /etc/nginx/sites-available/ を編集して、certbot を入れて、cron で更新を仕込んで……と頭の中で手順を組み立てた瞬間に、急にやる気が萎えた。何度やっても面倒なんだ、この作業。
そこで思い出したのが Caddy だった。「Let's Encrypt 自動取得・自動更新が標準装備」と聞いてはいたが、ちゃんと触るのは初めて。結果から言うと、設定ファイル3行で HTTPS 化が完了した。比喩じゃなくて、本当に3行。
その勢いで自宅の他のサービスも全部 Caddy に移した。半年使い込んだ正直な感想と、ハマりどころを書いておく。
Caddy とは何か——nginx と何が違うのか
Caddy は Go 製のWebサーバ兼リバースプロキシ。最大の特徴は HTTPS が標準でオン という思想。ドメイン名を書くだけで、勝手に Let's Encrypt から証明書を取って、勝手に自動更新する。certbot を別で動かす必要がない。
nginx との比較で言うと、こんな感じ。
| 項目 | nginx | Caddy |
|---|---|---|
| HTTPS | certbot 等で別途設定 | 標準・自動 |
| 設定ファイル | nginx.conf(独自構文) | Caddyfile(簡潔・JSON も可) |
| HTTP/3 (QUIC) | experimental | 標準 |
| パフォーマンス | 非常に高速 | nginx よりやや劣る場面あり |
| 学習コスト | 高い | 低い |
大規模サイトで秒間数万リクエストを捌くなら nginx に分がある。ただ自宅サーバや小〜中規模の運用では、Caddy の手軽さが圧倒的に効く。
インストール(Ubuntu / Raspberry Pi OS)
公式の APT リポジトリを追加するのが一番楽。
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
インストールすると caddy.service が systemd に自動登録される。Raspberry Pi 5 でも問題なく ARM64 バイナリが落ちてくる。
確認はこれだけ。
systemctl status caddy
caddy version
最小構成——本当に3行で HTTPS 化
設定ファイルは /etc/caddy/Caddyfile。インストール直後はデフォルトで :80 リスンするだけのサンプルが入っているので、それを書き換える。
仮に手元のドメインが dash.example.com で、内部の Grafana が localhost:3000 で動いているとすれば——
dash.example.com {
reverse_proxy localhost:3000
}
これで終わり。sudo systemctl reload caddy を叩いて30秒ほど待つと、ACME チャレンジが完走して証明書が降ってくる。https://dash.example.com がそのまま開く。
初めてやったときは「いや、ちゃんと検証されたのか?」と疑って journalctl -u caddy -f を眺めた。certificate obtained successfully のログが流れてきて、本当に終わってた。拍子抜けした。
前提条件: ドメインの A レコードがサーバの公開IPを向いていて、80番と443番ポートが外部から到達可能であること。これがないと Let's Encrypt の HTTP-01 チャレンジが失敗する。
複数サービスを1台で捌く
自宅サーバの本領は、1台で複数のサービスをホストすること。Caddyfile はサイトブロックを並べるだけでいい。
dash.example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:8000
}
files.example.com {
root * /srv/files
file_server browse
}
# パス単位の振り分けも可能
app.example.com {
handle /api/* {
reverse_proxy localhost:8080
}
handle {
reverse_proxy localhost:5173
}
}
このシンプルさが効く。nginx で同じことをやろうとすると server ブロックを書いて、listen 443 ssl を書いて、証明書のパスを書いて、location ブロックを切って——となる。Caddyfile は 意図がそのまま設定になる。
運用で実際に役立った機能
basic_auth で簡易認証
家族にだけ見せるダッシュボードに認証を付けたかった。Caddy なら1行。
dash.example.com {
basic_auth {
family JDJhJDE0JEh...
}
reverse_proxy localhost:3000
}
ハッシュは caddy hash-password で生成する。bcrypt なので強度的にも問題ない。OAuth みたいな大掛かりな認証基盤を立てるほどでもない、というシーンに刺さる。
自動 HTTP→HTTPS リダイレクト
これも自動。何も書かなくても、80番ポートに来たリクエストは443にリダイレクトされる。「うっかり http:// で叩いてしまった」を救ってくれる。
ログのアクセス記録
サイトブロック内に log ディレクティブを書くだけ。
dash.example.com {
log {
output file /var/log/caddy/dash.log
format json
}
reverse_proxy localhost:3000
}
JSON で吐けるので、ローカルで jq 集計するのも楽。journalctl 単体だとフィルタが面倒なので、サイトごとに分けると後で助かる。 jq の実践テクニック はこのあたりで使う。
nginx から移行してハマったこと
1. WebSocket は意外と引っかかる
素朴に reverse_proxy を書いただけでは、一部の WebSocket アプリで切断が発生した。Caddy の v2 系は WebSocket を自動でアップグレードするはずなのだが、ヘッダ周りで噛み合わないケースがあった。
結局こう書いて解決した。
chat.example.com {
reverse_proxy localhost:8765 {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
正直、ここは原因をちゃんと追い切れていない。ベースの WebSocket は通るのに、特定のフレームワークだけ駄目だった。再現条件をまとめて GitHub issue を漁る予定。
2. レスポンスのバッファリング
長時間のストリーミングレスポンス(SSE)で、終端側に届くタイミングが妙に遅れる現象に遭遇した。flush_interval を明示すれば直る。
sse.example.com {
reverse_proxy localhost:9000 {
flush_interval -1
}
}
nginx の proxy_buffering off 相当だ。デフォルトでこうしておいてくれてもいい気はする。
3. systemd ユニットの権限
Caddy の systemd ユニットは caddy ユーザで動く。/var/log/caddy/ へのログ出力は問題ないが、自前のディレクトリ(例えば /srv/files)を file_server で配る場合、所有者・読み取り権限を caddy に合わせる必要がある。これを忘れて30分ハマった。
systemd 周りの調査は journalctl 実践ガイド でも書いたが、journalctl -u caddy --since "10 min ago" で見れば原因はだいたい分かる。
外部公開しない場合の使いどころ
面白いのは、外に公開しない用途でも Caddy は便利 ということ。
たとえば自分の場合、 Tailscale で家のネットワークに入る 構成にしているので、外には何も穴を開けていない。それでも内部用の *.ts.net ドメインに対して Caddy で HTTPS 化している。tls internal を書くと自己署名証明書を発行してくれて、内部だけで完結する。
{
auto_https disable_redirects
}
grafana.tailnet-xxxx.ts.net {
tls internal
reverse_proxy localhost:3000
}
Tailscale の MagicDNS と組み合わせると、家の中のサービスが全部 https:// で参照できる。地味だが、ブラウザの「保護されていない通信」警告が消えるだけで気分が違う。
Cloudflare Tunnel と並用しているサービスもある。 Cloudflare Tunnel の構築手順 を別記事で書いたが、用途で使い分けている。外部公開が必要なものは Tunnel、内部だけで使うものは Caddy 単体、という分け方。
パフォーマンスは正直どうなのか
自宅レベルでは差を感じない。Raspberry Pi 5 で wrk -t4 -c100 -d30s を投げてみると、静的ファイル配信で約 38,000 req/s、リバースプロキシ越しで約 12,000 req/s だった。家族3人が同時にアクセスする程度の負荷では誤差にもならない。
ベンチマーク結果は環境とテスト条件次第なので、参考程度に見てほしい。本番のWebサービスで nginx -> Caddy 移行を検討しているなら、自分のワークロードで実測すべき。
ちなみに自分は外部公開用のサービスを お名前.comの高性能VPS
に置いていて、そこにも Caddy を入れて運用している。家から離れた場所で動かす用途でも、設定の単純さは効く。
nginx を選ぶべきケース
Caddy 推しではあるが、無条件におすすめはしない。次のケースは nginx の方が向いている。
- 秒間数万リクエストを捌く規模——枯れた最適化の蓄積で nginx に分がある
- 細かいキャッシュ制御が必要なケース(
proxy_cache_path周りの柔軟性) - 既存のチームが nginx に習熟していて、移行コストが見合わない
- サードパーティのモジュール(Lua スクリプティングなど)を使っている
逆に、自宅サーバ・個人プロジェクト・スタートアップ初期のサービスなら、Caddy の手軽さは時間を返してくれる。設定の短さは正義。
結局どう運用しているか
今の自分の構成はこう。
- 自宅 Raspberry Pi 5: Caddy + Tailscale + 内部用サービス(HTTPS は内部証明書)
- 外部 VPS: Caddy + 外部公開API(Let's Encrypt)
- 一部のサービス: Cloudflare Tunnel 経由(DNS 周りを Cloudflare に寄せたいもの)
Caddyfile は git 管理して、caddy validate --config /etc/caddy/Caddyfile で構文チェックしてから reload する流れ。validate を CI に組み込んでおくと、設定ミスでサイトが全停止する事故が防げる。
nginx に戻る理由は、今のところ思いつかない。手元のスケールには合わないからだ。「設定が短い」「自動 HTTPS」「systemd と素直に噛み合う」。この3つだけで自宅運用ならもう十分。
もし自宅サーバを始めたばかりで HTTPS 化に手こずっているなら、最初に試すツールとして真剣に推せる。少なくとも自分は、最初から Caddy にしておけば良かった、と何度か思った。