Caddy 优缺点
最近了解了一下 Caddy,准备从 Nginx 转到 Caddy。本文指的 Caddy 均为 Caddy 2。
Caddy 的优点有:
- 自动申请 TLS 证书(一大卖点!)
- 语法简洁
缺点有:
- 插件较 Nginx 少
- 文档不多,而且网上的讨论也不多
安装 Caddy
安装好以后,编写一个简单的 Caddyfile 用于测试:
mkdir /etc/caddy
vim /etc/caddy/Caddyfile
输入如下内容,然后保存:
:2015
Hello world!
启用 Caddy 服务并启动,然后查看其状态:
启用
sudo systemctl daemon-reload
sudo systemctl enable caddy
启动
sudo systemctl start caddy
查看状态
systemctl status caddy
如果是 active (running),则安装成功!如果是 failed,请检查 Caddyfile 的位置是否正确(按官方的配置,应该是 /etc/systemd/system/caddy.service)。
验证一下网站服务,curl 获取网站内容:
curl localhost:2015
如果返回 Hello world! 即正确。
以后,如果修改了 Caddyfile,使用 systemctl reload caddy 即可使其重新读取配置文件。
运行 Caddy
Caddy 可以由用户运行,也可以由 caddy 用户以 systemctl 的形式在后台运行。
由于 systemctl 运行出错时的提示很少,推荐学习、测试的时候使用用户身份运行,测试完成以后使用 systemctl。
以用户身份运行及停止:
caddy start
caddy stop
这两条命令会读取当前目录的 Caddyfile,所以记得提前切换到 /etc/caddy。
以系统身份运行及停止:
systemctl start caddy
systemctl stop caddy
查看调试信息:
systemctl status caddy
两种方法的重新加载分别为:
caddy reload
或者
systemctl reload caddy
Caddyfile 常见配置
在不进行额外设置的情况下,Caddy 都是 443 端口自动申请 HTTPS,80 端口重定向到 443 端口的。
常见配置 ,入门 Caddyfile 时也可以参考一下,对 Caddyfile 有个基本的认识。
Caddyfile 入门
Hello World!
如果只打算定义一个网站,Caddyfile 的第一行是网址,后面的就是一个或多个指令 directive:
localhost
respond "Hello, world!"
将上述文本保存在 /etc/caddy/Caddyfile,然后使用 systemctl reload caddy 重新读取后,就可以尝试用浏览器或 curl 打开该网站:
$ curl https://localhost
Hello, world!
定义多个网站
一个文件可以定义多个网站。但是需要将上述语法改为下面的等价语法:
localhost { # 大括号前必须有空格
respond "Hello, world!"
}
就可以在多个语法块中定义每个网站了。
import 其他配置文件
也可以在多个文件中定义配置。如在 a.txt 写入以下内容:
a.com {
respond "Hello, world!"
}
在 b.txt 写入以下内容:
b.com {
respond "Nice to meet you!"
}
然后在 Caddyfile 中写入:
以 Caddyfile 形式导入两个 txt
import a.txt
import b.txt
即可。
和 Nginx 一样,你需要提前将 a.com 和 b.com 的域名解析以 A 形式指向你的服务器 IP。
静态网站 file_server
example.com {
root * /var/www
encode gzip zstd
file_server
}
访问 https://example.com 会看到服务器 /var/www/ 的内容。如果存在 index.html,则会打开这个网页。还可指定别的 index:
example.com {
root * /var/www
encode gzip zstd
file_server {
index www.index.html
}
}
还可以使用浏览器浏览文件夹 + 重定向 + 加上反斜线。
example.com {
redir /v /v/
handle /v/* {
uri strip_prefix /v
encode gzip zstd
file_server browse {
root /etc/zqxt_app/getproxy/
}
}
}
反向代理 reverse_proxy
example.com {
reverse_proxy localhost:5000
}
三行即可。访问 https://example.com 实际上访问的是服务器的 5000 端口。
利用以下配置可将 https://example.com/proxy 反向代理到 localhost:5000。
example.com {
reverse_proxy /proxy localhost:5000
}
还可利用以下配置可将 https://example.com 反向代理到 localhost:5000/proxy。
example.com {
uri replace / /proxy 1
reverse_proxy localhost:5000
}
上述配置表示,在反向代理之前,将 uri 的前 1 个 / 替换为 /proxy。
也可以修改 header:
example.com {
uri replace / /proxy 1
reverse_proxy localhost:5000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
重定向 redir
www.example.com {
redir https://example.com{uri}
}
访问 www.example.com 会 302 Redirect 重定向到 https://example.com。
也可以使用 permanent:
www.example.com {
redir https://example.com{uri} permanent
}
访问 www.example.com 会 301 Move permanently 重定向到 https://example.com。
按顺序执行 route
上述语法 file_server、reverse_proxy、redir 可以混合使用。
example.com {
reverse_proxy /proxy localhost:5000
redir /github github.com
redir /google google.com
file_server * /var/www/html/
}
这会使得 example.com/proxy、example.com/github、example.com/google 以及 example.com 的其他地址执行对应的功能。
但是,默认情况下,在执行过程中,指令的执行顺序会根据指令名进行调整。比如,file_server 是最后执行的。如果改成以下代码,file_server 则不会被运行,因为在执行到 file_server 之前,/google 已经被重定向了。
example.com {
reverse_proxy /proxy localhost:5000
redir /github github.com
file_server /google /var/www/html/
redir * google.com
}
如果真的需要这么做,可以将所有命令包含在 route 的语句块中。语句块中的内容将被顺序执行:
example.com {
route {
reverse_proxy /proxy localhost:5000
redir /github github.com
file_server /google /var/www/html/
redir * google.com
}
}
处理 handle
handle 类似于 Nginx 中的 location,是一种类似于分支逻辑的 HTTP handler logic(HTTP 处理逻辑)。
example.com {
handle /foo/* {
file_server
}
handle / {
reverse_proxy 127.0.0.1:8080
}
}
上述语法就会将 /foo 下面的网址以 file_server 运行,对其他则会进行反向代理。实现的功能和 route 类似,不过我猜测 handle 处理这类问题更高效。
定义错误页面 handle_errors
基于静态网站定义 404 页面的代码如下:
example.com {
root * /var/www/
file_server
handle_errors {
rewrite * /{http.error.status_code}.html
file_server
}
}
访问到不存在的网址则会显示 /404.html 的内容(但网址不会变化,仍然是访问的网址)。
rewrite url try_files
这三条命令都是修改 uri 用。
rewrite 将匹配的 uri 修改为指定的 uri;
uri 在原 uri 上进行修改;
try_files 将 uri 修改为列出路径中,路径对应的文件(文件夹)存在的第一项。
修改以后,只是访问的路径变化,并不会在地址栏有所体现。
example1.com {
rewrite * /foo.html # 将所有路径修改为 foo.html
}
example2.com {
uri replace / /proxy 1 # 将第一个 / 修改为 /proxy,即在链接前添加一个 /proxy
reverse_proxy localhost:9000 # example2.com/ => localhost:9000/proxy
}
example2.com {
route /api/* {
uri strip_prefix /api
reverse_proxy localhost:9000
}
}
example3.com {
try_files {path} /index.php # 如果 {path} 存在,访问 {path};否则访问 index.php
}
占位符 placeholders
上文的 {uri} 即是一种占位符。更多的占位符可见:https://caddyserver.com/docs/caddyfile/concepts#placeholders
匹配串 matchers
配置文件中一个很重要的部分是匹配串(matchers)如 / /proxy。在 https://caddyserver.com/docs/caddyfile/matchers 做了详细介绍。
泛域名 HTTPS?
当然,以上域名只做到了单域名 HTTPS,对于泛域名解析 HTTPS 则会麻烦得多。这是由于在 Let's Encrypt,单域名 HTTPS 证书可以使用 HTTP 验证(在网站对应的指定路径放一个指定的 HTML),而泛域名 (如*.example.com HTTPS 证书则需要使用 DNS 验证(在指定的域名下放一个 DNS 解析)。前者交给 Caddy 做非常方便,而后者就不那么方便了。
对于该问题,有以下解决办法:
Caddy 2 的确可以通过插件调用各 DNS 提供商的 API 来实现修改 DNS,但是 Caddy 2 官方的插件 (opens new window)只有很少,像国内阿里云、腾讯云都没有开发。貌似可以在阿里云将域名的 DNS 解析服务器改为 CloudFlare 的,然后利用 CloudFlare API 实现,但是这里我没有深入研究。
相比于 Caddy 2,Caddy 1 (opens new window)就提供了不少插件(但仍然没有阿里云)。如有需求可改为 Caddy 1。
使用 On-Demand TLS (opens new window)技术。这是 Caddy 研发的一种技术。
在第一次 TLS 握手时,如果发现本地没有 HTTPS 证书或已过期以后,就立刻向 Let's Encrypt 申请证书,成功后保存该证书,并完成此次握手;此后如果发现存在 HTTPS 证书且有效,就会使用该证书完成握手。
该方法的优点非常明显,就是可以用对访问到的每个域名申请 HTTPS 证书的方法,代替 DNS 验证。缺点也很明显,由于 HTTPS 证书申请需要时间,再加上国内网络问题,在首次访问某域名时,需要等待 10-40 秒不等供 Caddy 服务器申请证书。
我最终采用了这种方式,因为使用泛域名解析只是为了跳转到主域名上,实际上没有几个人会访问这些域名的。
On-Demand TLS 语法如下:
*.example.com {
tls {
on_demand
}
redir https://example.com{uri} permanent
}
最后一种无奈之举,就是不使用 HTTPS。如果该域名只是做一个 redir,其实不使用 HTTP 也还可以接受。
Caddy 不使用 HTTPS 的语法是,在域名后指定 80 端口。
*.example.com:80 {
redir https://example.com{uri} permanent
}
# or
http://*.example.com {
redir https://example.com{uri} permanent
}
但这两种写法都有个 bug:如果前面定义了 a.example.com { ... },访问 http://a.example.com 会被重定向到 https://example.com,而不是 https://a.example.com。