Caddy 簡介
Caddy 是一個 Go 編寫的 Web 服務器,類似于 Nginx,Caddy 提供了更加強大的功能,隨著 v2 版本發布 Caddy 已經可以作為中小型站點 Web 服務器的另一個選擇;相較于 Nginx 來說使用 Caddy 的優勢如下:
- 自動的 HTTPS 證書申請(ACME HTTP/DNS 挑戰)
- 自動證書續期以及 OCSP stapling 等
- 更高的安全性包括但不限于 TLS 配置以及內存安全等
- 友好且強大的配置文件支持
- 支持 API 動態調整配置(有木有人可以搞個 Dashboard)
- 支持 HTTP3(QUIC)
- 支持動態后端,例如連接 Consul、作為 k8s ingress 等
- 后端多種負載策略以及健康檢測等
- 本身 Go 編寫,高度模塊化的系統方便擴展(CoreDNS 基于 Caddy1 開發)
- ……
就目前來說,Caddy 對于我個人印象唯一的缺點就是性能沒有 Nginx 高,但是這是個仁者見仁智者見智的問題;相較于提供的這些便利性,在性能可接受的情況下完全有理由切換到 Caddy。
編譯 Caddy2
注意: 在 Caddy1 時代,Caddy 官方發布的預編譯二進制文件是不允許進行商業使用的,Caddy2 以后已經全部切換到 Apache 2.0 License。
在默認情況下 Caddy2 官方提供了預編譯的二進制文件,以及自定義 build 下載頁面,不過對于需要集成一些第三方插件時,我們仍需采用官方提供的 xcaddy 來進行自行編譯;以下為具體的編譯過程:
Golang 環境安裝
本部分編譯環境默認為 Ubuntu 20.04 系統,同時使用 root 用戶,其他環境請自行調整相關目錄以及配置;編譯時自行處理好科學上網相關配置,也可以直接用國外 VPS 服務器編譯。
首先下載 go 語言的 SDK 壓縮包,其他平臺可以從 https://golang.org/dl/ 下載對應的壓縮包:
wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz
下載完成后解壓并配置相關變量:
# 解壓
tar -zxvf go1.15.6.linux-amd64.tar.gz
# 移動到任意目錄
mkdir -p /opt/devtools
mv go /opt/devtools/go
# 創建 go 相關目錄
mkdir -p ${HOME}/gopath/{src,bin,pkg}
# 調整變量配置,將以下變量加入到 shell 初始化配置中
# bash 用戶請編輯 ~/.bashrc
# zsh 用戶請編輯 ~/.zshrc
export GOROOT='/opt/devtools/go'
export GOPATH="${HOME}/gopath"
export GOPROXY='https://goproxy.cn' # 如果已經解決了科學上網問題,GOPROXY 變量可以刪除,否則可能會起反作用
export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"
# 讓配置生效
# bash 用戶替換成 ~/.basrc
# 重新退出登錄也可以
source ~/.zshrc
配置完成后,應該在命令行執行 go version 并有以下成功返回:
bleem ? ~ go version
go version go1.15.6 linux/amd64
安裝 xcaddy
按照官方文檔直接命令行執行 go get -u github.com/caddyserver/xcaddy/cmd/xcaddy 安裝即可:
bleem ? ~ go get -u github.com/caddyserver/xcaddy/cmd/xcaddy
go: downloading github.com/caddyserver/xcaddy v0.1.7
go: found github.com/caddyserver/xcaddy/cmd/xcaddy in github.com/caddyserver/xcaddy v0.1.7
go: downloading github.com/Masterminds/semver/v3 v3.1.0
go: github.com/Masterminds/semver/v3 upgrade => v3.1.1
go: downloading github.com/Masterminds/semver/v3 v3.1.1
.....
安裝完成后應當在命令行可以直接執行 xcaddy 命令:
# xcaddy 并沒有提供完善的命令行支持,所以 `--help` 報錯很正常
bleem ? ~ xcaddy --help
go: cannot match "all": working directory is not part of a module
2021/01/07 12:15:56 [ERROR] exec [go list -m -f={{if .Replace}}{{.Path}} => {{.Replace}}{{end}} all]: exit status 1:
編譯 Caddy2
編譯之前系統需要安裝 jq、curl、git 命令,沒有的請使用
apt install -y curl git jq
命令安裝;
自行編譯的目的是增加第三方插件方便使用,其中官方列出的插件可以從 Download 頁面獲取到:
其他插件可以從 GitHub 上尋找或者自行編寫,整理好這些插件列表以后只需要使用 xcaddy 編譯即可:
# 獲取最新版本號,其實直接去 GitHub realse 頁復制一下就行
# 這里轉化為腳本是為了方便自動化
export version=$(curl -s "https://api.github.com/repos/caddyserver/caddy/releases/latest" | jq -r .tag_name)
# 使用 xcaddy 編譯
xcaddy build ${version} --output ./caddy_${version}
--with github.com/abIOSoft/caddy-exec
--with github.com/caddy-dns/cloudflare
--with github.com/caddy-dns/dnspod
--with github.com/caddy-dns/duckdns
--with github.com/caddy-dns/gandi
--with github.com/caddy-dns/route53
--with github.com/greenpau/caddy-auth-jwt
--with github.com/greenpau/caddy-auth-portal
--with github.com/greenpau/caddy-trace
--with github.com/hairyhenderson/caddy-teapot-module
--with github.com/kirsch33/realip
--with github.com/porech/caddy-maxmind-geolocation
--with github.com/caddyserver/format-encoder
--with github.com/mholt/caddy-webdav
編譯過程日志如下所示,稍等片刻后將會生成編譯好的二進制文件:
編譯成功后可以通過 list-modules 子命令查看被添加的插件是否成功編譯到了 caddy 中:
bleem ? ~ ./caddy_v2.3.0 list-modules
admin.api.load
admin.api.metrics
caddy.adapters.caddyfile
caddy.listeners.tls
caddy.logging.encoders.console
caddy.logging.encoders.filter
caddy.logging.encoders.filter.delete
caddy.logging.encoders.filter.ip_mask
caddy.logging.encoders.formatted
caddy.logging.encoders.json
caddy.logging.encoders.logfmt
caddy.logging.encoders.single_field
caddy.logging.writers.discard
caddy.logging.writers.file
caddy.logging.writers.net
caddy.logging.writers.stderr
caddy.logging.writers.stdout
caddy.storage.file_system
dns.providers.cloudflare
dns.providers.dnspod
dns.providers.duckdns
dns.providers.gandi
dns.providers.route53
exec
http
http.authentication.hashes.bcrypt
http.authentication.hashes.scrypt
http.authentication.providers.http_basic
http.authentication.providers.jwt
......
安裝 Caddy2
宿主機安裝
宿主機安裝 Caddy2 需要使用 systemd 進行守護,幸運的是 Caddy2 官方提供了各種平臺的安裝包以及 systemd 配置文件倉庫;目前推薦的方式是直接采用包管理器安裝標準版本的 Caddy2,然后替換自編譯的可執行文件:
# 安裝標準版本 Caddy2
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/gpg/gpg.155B6D79CA56EA34.key' | sudo apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/setup/config.deb.txt?distro=debian&version=any-version' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
# 替換二進制文件
systemctl stop caddy
rm -f /usr/bin/caddy
mv ./caddy_v2.3.0 /usr/bin/caddy
Docker 安裝
Docker 用戶可以通過 Dockerfile 自行編譯 image,目前我編寫了一個基于 xcaddy 的 Dockerfile,如果有其他插件需要集成自行修改重新編譯即可;當前 Dockerfile 預編譯的鏡像已經推送到了 Docker Hub 中,鏡像名稱為 mritd/caddy。
配置 Caddy2
Caddy2 的配置文件核心采用 json,但是 json 可讀性不強,所以官方維護了一個轉換器,抽象出稱之為 Caddyfile 的新配置格式;關于 Caddyfile 的完整語法請查看官方文檔 https://caddyserver.com/docs/caddyfile,本文僅做一些基本使用的樣例。
配置片段
Caddyfile 支持類似代碼中 function 一樣的配置片段,這些配置片段可以在任意位置被 import,同時可以接受參數,以下為配置片斷示例:
# 括號內為片段名稱,可以自行定義
(TLS) {
protocols tls1.2 tls1.3
ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}
# 在任意位置可以引用此片段從而達到配置復用
import TLS
配置模塊化
import 指令除了支持引用配置片段以外,還支持引用外部文件,同時支持通配符,有了這個命令以后我們就可以方便的將配置文件進行模塊化處理:
# 引用外部的 /etc/caddy/*.caddy
import /etc/caddy/*.caddy
站點配置
針對于站點域名配置,Caddyfile 比較自由化,其格式如下:
地址 {
站點配置
}
關于這個 “地址” 接受多種格式,以下都為合法的地址格式:
localhost
example.com
:443
http://example.com
localhost:8080
127.0.0.1
[::1]:2015
example.com/foo/*
*.example.com
http://
環境變量
Caddyfile 支持直接引用系統環境變量,通過此功能可以將一些敏感信息從配置文件中剔除:
# 引用環境變量 GANDI_API_TOKEN
dns gandi {$GANDI_API_TOKEN}
配置片段參數支持
針對于配置片段,Caddyfile 還支持類似于函數代碼的參數支持,通過參數支持可以讓外部引用時動態修改配置信息:
(LOG) {
log {
format json {
time_format "iso8601"
}
# "{args.0}" 引用傳入的第一個參數,此處用于動態傳入日志文件名稱
output file "{args.0}" {
roll_size 100mb
roll_keep 3
roll_keep_for 7d
}
}
}
# 引用片段
import LOG "/data/logs/mritd.com.log"
自動證書申請
在啟動 Caddy2 之前,如果目標域名(例如: www.example.com)已經解析到了本機,那么 Caddy2 啟動后會嘗試自動通過 ACME HTTP 挑戰申請證書;如果期望使用 DNS 的方式申請證書則需要其他 DNS 插件支持,比如上面編譯的 --with github.com/caddy-dns/gandi 為 gandi 服務商的 DNS 插件;關于使用 DNS 挑戰的配置編寫方式需要具體去看其插件文檔,目前 gandi 的配置如下:
tls {
dns gandi {env.GANDI_API_TOKEN}
}
配置完成后 Caddy2 會通過 ACME DNS 挑戰申請證書,值得注意的是即使通過 DNS 申請證書默認也不會申請泛域名證書,如果想要調整這種細節配置請使用 json 配置或管理 API。
完整模塊化配置樣例
了解了以上基礎配置信息,我們就可以實際編寫一個站點配置了;以下為本站的 Caddy 配置樣例:
目錄結構:
caddy
├── Caddyfile
├── mritd.com.caddy
└── mritd.me.caddy
Caddyfile
Caddyfile 主要包含一些通用的配置,并將其抽到配置片段中,類似于 nginx 的 nginx.conf 主配置;在最后部分通過 import 關鍵字引入其他具體站點配置,類似 nginx 的 vhost 配置。
(LOG) {
log {
# 日志格式參考 https://github.com/caddyserver/format-encoder 插件文檔
format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}" {
time_format "iso8601"
}
output file "{args.0}" {
roll_size 100mb
roll_keep 3
roll_keep_for 7d
}
}
}
(TLS) {
# TLS 配置采用 https://mozilla.github.io/server-side-tls/ssl-config-generator/ 生成,SSL Labs 評分 A+
protocols tls1.2 tls1.3
ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}
(HSTS) {
# HSTS (63072000 seconds)
header / Strict-Transport-Security "max-age=63072000"
}
(ACME_GANDI) {
# 從環境變量獲取 GANDI_API_TOKEN
dns gandi {$GANDI_API_TOKEN}
}
# 聚合上面的配置片段為新的片段
(COMMON_CONFIG) {
# 壓縮支持
encode zstd gzip
# TLS 配置
tls {
import TLS
import ACME_GANDI
}
# HSTS
import HSTS
}
# 開啟 HTTP3 實驗性支持
{
servers :443 {
protocol {
experimental_http3
}
}
}
# 引入其他具體的站點配置
import /etc/caddy/*.caddy
mritd.com.caddy
mritd.com.caddy 為主站點配置,主站點配置內主要編寫一些路由規則,TLS 等都從配置片段引入,這樣可以保持統一。
www.mritd.com {
# 重定向到 mritd.com(默認 302)
redir https://mritd.com{uri}
# 日志
import LOG "/data/logs/mritd.com.log"
# TLS、HSTS、ACME 等通用配置
import COMMON_CONFIG
}
mritd.com {
# 路由
route /* {
reverse_proxy mritd_com:80
}
# 日志
import LOG "/data/logs/mritd.com.log"
# TLS、HSTS、ACME 等通用配置
import COMMON_CONFIG
}
mritd.me.caddy
mritd.me.caddy 為老站點配置,目前主要將其 301 到新站點即可。
www.mritd.me {
# 重定向到 mritd.com
# 最后的 "code" 支持三種參數
# temporary => 302
# permanent => 301
# html => HTML document redirect
redir https://mritd.com{uri} permanent
# 日志
import LOG "/data/logs/mritd.com.log"
# TLS、HSTS、ACME 等通用配置
import COMMON_CONFIG
}
mritd.me {
# 重定向
redir https://mritd.com{uri} permanent
# 日志
import LOG "/data/logs/mritd.com.log"
# TLS、HSTS、ACME 等通用配置
import COMMON_CONFIG
}
啟動與重載
配置文件編寫完成后,通過 systemctl start caddy 可啟動 caddy 服務器;每次配置修改后可以通過 systemctl reload caddy 進行配置重載,重載期間 caddy 不會重啟(實際上調用 caddy reload 命令),當配置文件書寫錯誤時,重載只會失敗,不會影響正在運行的 caddy 服務器。
總結
本文只是列舉了一些簡單的 Caddy 使用樣例,在強大的插件配合下,Caddy 可以實現各種 “神奇” 的功能,這些功能依賴于復雜的 Caddy 配置,Caddy 配置需要仔細閱讀官方文檔,關于 Caddyfile 的每個配置段在文檔中都有詳細的描述。
值得一提的是 Caddy 本身內置了豐富的插件,例如內置 “file_server”、內置各種負載均衡策略等,這些插件組合在一起可以實現一些復雜的功能;Caddy 是采用 go 編寫的,官方也給出了詳細的開發文檔,相較于 Nginx 來說通過 Lua 或者 C 來開發編寫插件來說,Caddy 的插件開發上手要容易得多;Caddy 本身針對數據存儲、動態后端、配置文件轉換等都內置了擴展接口,這為有特定需求的擴展開發打下了良好基礎。
最終總結,綜合來看目前 Caddy2 的性能損失可接受的情況下,相較于 Nginx 絕對是個絕佳選擇,各種新功能都能夠滿足現代化 Web 站點的需求,真香警告。