# Caddy 优缺点

最近了解了一下 Caddy,准备从 Nginx 转到 Caddy。本文指的 Caddy 均为 Caddy 2。

Caddy 官方文档 (opens new window) Caddy 中文文档 (opens new window)

Caddy 的优点有:

  • 自动申请 TLS 证书(一大卖点!)
  • 语法简洁

缺点有:

  • 插件较 Nginx 少
  • 文档不多,而且网上的讨论也不多

# 安装 Caddy

参考链接:下载 (opens new window) | 安装 (opens new window)

由于 yum 自带的是 Caddy 1,而我按官网从 yum 下载 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 端口的。

常见配置 (opens new window),入门 Caddyfile 时也可以参考一下,对 Caddyfile 有个基本的认识。

# Caddyfile 入门

官方教程 (opens new window)

# 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 其他配置文件

参考 (opens new window)

也可以在多个文件中定义配置。如在 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.comb.com 的域名解析以 A 形式指向你的服务器 IP。

# 静态网站 file_server

参考 (opens new window)

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/uestcmsc_webapp/getproxy/
        }
    }
}

# 反向代理 reverse_proxy

参考 (opens new window)

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

参考 (opens new window)

www.example.com {
	redir https://example.com{uri}
}

访问 www.example.com302 Redirect 重定向到 https://example.com

也可以使用 permanent

www.example.com {
	redir https://example.com{uri} permanent
}

访问 www.example.com301 Move permanently 重定向到 https://example.com

# 按顺序执行 route

参考 (opens new window)

上述语法 file_serverreverse_proxyredir 可以混合使用。

example.com {
    reverse_proxy /proxy localhost:5000
    redir         /github github.com
    redir         /google google.com
    file_server   *      /var/www/html/
}

这会使得 example.com/proxyexample.com/githubexample.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

参考 (opens new window)

基于静态网站定义 404 页面的代码如下:

example.com {
    root * /var/www/
    file_server
    handle_errors {
        rewrite * /{http.error.status_code}.html
        file_server
    }
}

访问到不存在的网址则会显示 /404.html 的内容(但网址不会变化,仍然是访问的网址)。

# rewrite url try_files

参考 (opens new window)

这三条命令都是修改 uri 用。

  • rewrite 将匹配的 uri 修改为指定的 uri;
  • uri 在原 uri 上进行修改;
  • try_filesuri 修改为列出路径中,路径对应的文件(文件夹)存在的第一项。

修改以后,只是访问的路径变化,并不会在地址栏有所体现

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 做非常方便,而后者就不那么方便了。

对于该问题,有以下解决办法:

  1. Caddy 2 的确可以通过插件调用各 DNS 提供商的 API 来实现修改 DNS,但是 Caddy 2 官方的插件 (opens new window)只有很少,像国内阿里云、腾讯云都没有开发。貌似可以在阿里云将域名的 DNS 解析服务器改为 CloudFlare 的,然后利用 CloudFlare API 实现,但是这里我没有深入研究。

  2. 相比于 Caddy 2,Caddy 1 (opens new window) 就提供了不少插件(但仍然没有阿里云)。如有需求可改为 Caddy 1。

  3. 使用 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
}
  1. 最后一种无奈之举,就是不使用 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