最近试着用了下 Caddy,自动申请证书确实非常方便,就是中文文档少了点。虽然官方文档写得很详细,社区里提问也基本有问必答,奈何都是英文,而 I english was very bad。所以这里记录一些使用过程中遇到的问题,以便以后忘了怎么用的时候,不必再跟 AI 大战三百回合。
安装
直接参考 Cyrus 佬的 docker-caddy 就行。如果需要其他模块,参照仓库内的 Dockerfile
修改一下,自己编译一个镜像。
伪静态
Typecho 是 PHP 驱动的博客程序,但可以启用伪静态以优化 URL,不过需要在 Web 服务器上处理一下。如果是 Nginx 的话,是这么写的:
try_files $uri $uri/ /index.html;
而 Caddy 可以直接通过 php_fastcgi
实现 Nginx 的 try_files
效果,不过根据官方文档对于 php_fastcgi 的解释,其实是这个指令自带了一些默认写法,包括 try_files
。例如:
@indexFiles file {
try_files {path} {path}/index.php index.php
try_policy first_exist_fallback
split_path .php
}
因此配置一个 Typecho 博客的时候,伪静态就可以直接这么写:
example.com {
encode zstd gzip
root * /srv/example.com
file_server
php_fastcgi php:9000
header {
-Server
-X-Powered-By
}
}
但是二级目录就行不通了,得手动写一下 try_files
指令。Nginx 是这么写的:
location /blog/ {
if (!-e $request_filename) {
rewrite ^(.*)$ /blog/index.php$1 last;
}
}
Caddy 也差不多。假设你的一级域名博客 example.com
放在了 /srv/example.com
这个文件夹中,而这个文件夹里又有个 blog
文件夹打算放另一个博客,即:
example.com {
encode zstd gzip
root * /srv/example.com
file_server
php_fastcgi php:9000
handle /blog/* {
try_files {path} /blog/index.php?{query}
}
}
注意要保证 /blog/index.php?{query}
前面的 blog
和文件夹名称一致。
然后伪静态就能正常工作了。
日志
参考《一行代码快速配置 Caddy 站点日志——复用 Caddy 配置段》,可以实现使用 Caddy 的代码段(Snippet)和占位符,为每个站点更优雅地配置日志。
但有一个问题是, 对于习惯了 Nginx 简短的一行日志的我来说,Caddy 的 JSON 日志并不优雅:
{"level":"info","ts":1753359481.0373497,"logger":"http.log.access.log19","msg":"handled request","request":{"remote_ip":"3.4.5.6","remote_port":"51262","client_ip":"1.2.3.4","proto":"HTTP/2.0","method":"GET","host":"example.com","uri":"/vcb-s","headers":{"Cf-Ipcountry":["US"],"Cdn-Loop":["cloudflare; loops=1"],"Accept":["application/rss+xml, application/rdf+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.8, text/*;q=0.7, application/*;q=0.6"],"X-Forwarded-Proto":["https"],"Cf-Connecting-Ip":["1.2.3.4"],"If-None-Match":["\"1138a-MnD/hvrofjXPGRMPTdAYBESwO4U\""],"If-Modified-Since":["Thu, 24 Jul 2025 04:34:01 +0000"],"Cf-Ray":["96435b8e8b406fd7-IAD"],"X-Forwarded-For":["1.2.3.4"],"User-Agent":["RSStT/2.10.0 RSS Reader (+https://git.io/RSStT)"],"Accept-Encoding":["gzip, br"],"Cf-Visitor":["{\"scheme\":\"https\"}"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"example.com"}},"bytes_read":0,"user_id":"","duration":0.926949864,"size":0,"status":304,"resp_headers":{"Via":["1.1 Caddy"],"Access-Control-Allow-Methods":["GET"],"X-Content-Type-Options":["nosniff"],"Date":["Thu, 24 Jul 2025 12:18:01 GMT"],"Alt-Svc":["h3=\":443\"; ma=2592000"],"Access-Control-Allow-Origin":["example.com"],"Cache-Control":["public, max-age=300"],"Content-Type":["application/xml; charset=utf-8"],"Etag":["\"1138a-MnD/hvrofjXPGRMPTdAYBESwO4U\""],"X-Rsshub-Route":["/vcb-s"]}}
至少不是人能看的。
我只是写写博客玩,用不上这么详细的日志。一番搜索,发现 Nginx 的日志样式叫做 Common Log Format (通用日志格式),而 Caddy 有一个模块可以实现这种日志样式。可以在构建镜像的时候自己塞进去:
FROM caddy:builder AS builder
RUN apk add --no-cache tzdata
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
# --with github.com/caddy-dns/dnspod \
# --with github.com/caddy-dns/alidns \
--with github.com/caddy-dns/tencentcloud \
# Extended plugins
--with github.com/mholt/caddy-dynamicdns \
--with github.com/caddyserver/replace-response \
# 加入日志模块
--with github.com/caddyserver/transform-encoder
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
然后在原日志代码段加上日志格式化配置:
(log) {
log {
output file /log/{args[0]}/access.log {
roll_size 5MiB
roll_local_time
roll_keep 10
roll_keep_for 2160h
}
format transform `{request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "2006-01-02 15:04:05"
time_local
}
}
}
日志就会变成这样:
4.5.6.7 - - [2025-07-25 10:11:37] "GET / HTTP/2.0" 200 1901 "-" "Uptime-Kuma/2.0.0-beta.3"
舒服多了。
只是这个模块被官方遗弃了,官方觉得 JSON 日志好使。
CA
Caddy 默认的 CA(证书颁发机构)是 Let's Encrypt,但也支持使用其他 CA,比如 ZeroSSL。而我们还可以通过在 Caddyfile 中使用 acme_ca
指令来指定其他 CA,比如 Google。
参考《使用 acme.sh 申请 Google 的免费 SSL 证书》获取 EAB 密钥后,在 Caddyfile
顶部加入以下配置:
acme_ca https://dv.acme-v02.api.pki.goog/directory
acme_eab {
key_id xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
mac_key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
email your@mail.com
随后添加域名,重载 Caddy 配置,尝试申请证书。
如果 Caddy 内的 /data/caddy/acme/
文件夹出现了名为 acme/dv.acme-v02.api.pki.goog-directory
的文件夹,且该文件夹内包含上面注册 ACME 账户时使用的邮箱,即表示账户注册成功。
随后要观察 /data/caddy/certificates/
是否文件夹出现了 dv.acme-v02.api.pki.goog-directory
文件夹。若该文件夹内部有域名证书生成,则表示证书申请成功。
文件结构大概是像这样:
data
└── caddy
├── acme
│ ├── acme-v02.api.letsencrypt.org-directory
│ │ └── users
│ │ └── your@mail.com
│ ├── acme.zerossl.com-v2-dv90
│ │ └── users
│ │ └── your@mail.com
│ └── dv.acme-v02.api.pki.goog-directory
│ └── users
│ └── your@mail.com
├── certificates
│ ├── acme-v02.api.letsencrypt.org-directory
│ │ └── example.com
│ │ ├── example.com.crt
│ │ ├── example.com.json
│ │ └── example.com.key
│ └── dv.acme-v02.api.pki.goog-directory
│ └── example.com
│ ├── example.com.crt
│ ├── example.com.json
│ └── example.com.key
├── instance.uuid
├── last_clean.json
├── locks
└── ocsp
不过这个 EAB 密钥使用一次后便会自动失效,不使用也会在七天内失效。但使用 EAB 密钥注册的 ACME 帐户没有过期时间,对证书续期没有影响。
所以待 ACME 账户注册成功后,就可以注释或删除相关秘钥,仅保留 acme_ca
配置:
acme_ca https://dv.acme-v02.api.pki.goog/directory
email your@mail.com
但使用 Google CA 除了能表明证书来自谷歌之外,没有其他优势。追求新鲜玩玩就够了,平常还是用 Let's Encrypt 或 ZeroSSL 比较合适。
再说 https://dv.acme-v02.api.pki.goog/directory 也被墙了,国内机子根本没法申请。
缓存
Caddy 并不能像 Nginx 一样在反代网站的时候配置缓存,需要手动安装一些模块。比如 cache-handler,一个分布式 HTTP 缓存模块,构建镜像的时候加上它:
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
# --with github.com/caddy-dns/dnspod \
--with github.com/caddy-dns/alidns \
--with github.com/caddy-dns/tencentcloud \
# Extended plugins
--with github.com/mholt/caddy-dynamicdns \
--with github.com/caddyserver/replace-response \
# 分布式 HTTP 缓存模块
--with github.com/caddyserver/cache-handler
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
使用倒是很简单:
{
cache
}
example.com {
cache
reverse_proxy your-app:8080
}
具体用法还是得查查相关文档,我不太会用。
但我在琢磨缓存的时候搜到了另一个模块,叫做 cdp-cache。它与 cache-handler 的区别在于,这个模块更容易理解和配置。
一样先构建镜像:
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/caddy-dns/tencentcloud \
# Extended plugins
--with github.com/mholt/caddy-dynamicdns \
--with github.com/caddyserver/replace-response \
# 第三方缓存模块
--with github.com/sillygod/cdp-cache
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
然后在网站配置中启用:
proxy.example.com {
reverse_proxy https://example.com
http_cache {
cache_type file
path /cache
match_path /images
default_max_age 24h
}
}
配置项不少,具体得参考官方文档。
我玩不来,用了一两天就删掉了。
robots.txt
如果有些网站你并不想被爬虫抓取,就可以使用 robots.txt,立下君子协定:
User-agent: *
Disallow: /
这玩意本质上就是个 txt
文本,所以可以在 Web 服务器端直接配置,例如 Nginx:
location = /robots.txt {
default_type text/html;
add_header Content-Type "text/plain; charset=UTF-8";
return 200 "User-Agent: *\nDisallow: /";
}
Caddy 也可以这么做,直接在 Caddyfile
中加入 robot_txt
配置块:
(robots_txt) {
respond /robots.txt 200 {
body "User-agent: *
Disallow: /"
}
}
然后在网站配置文件中导入该块:
example.com {
encode zstd gzip
root * /srv/blog
file_server
import robot_txt
}
IPv6
现在越来越多的服务器厂商开始支持 IPv6,我的服务器也提供了开启 IPv6 的选项。但 IPv6 其实坑不少,至少我不太会用。就比如我一昧追新开启了 IPv6 后,发现博客后台获取不到正确的访客 IP 地址,全部都是内网地址。
我甚至以为是 Nginx 的问题,查了半天,才发觉可能是 IPv6 的锅。
总之,AI 是这么解释的:
此问题的根源在于 Docker 守护进程未启用 IPv6,导致容器本身不具备原生的 IPv6 网络栈和地址。当外部 IPv6 流量通过端口映射到达宿主机时,Docker 的网络代理层(通常是docker-proxy
进程或iptables
规则)必须执行跨协议转发。在此过程中,它会终止原始的 TCPv6 连接,然后以 Docker 网桥的 IPv4 地址(如172.17.0.1
)为源地址,向容器的私有 IPv4 地址发起一个全新的 TCPv4 连接。最终,容器内的 Nginx 服务只能感知到这个新连接的直接来源,即作为代理的 Docker 网关,从而丢失了真实的客户端 IPv6 源地址信息。
所以,如果你的服务器也启用了 IPv6,为了让容器能正常识别来自 IPv6 的访问,请开启 Docker 的 IPv6 支持,并创建一个支持 IPv6 的 Docker 网络。
编辑 Docker 的配置文件(通常位于 /etc/docker/daemon.json
),加入或合并以下内容以开启 Docker 的 IPv6 支持:
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/48"
}
操作命令:
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<EOF
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/48"
}
EOF
重启 Docker 服务以应用更改:
sudo systemctl restart docker
随后创建一个支持 IPv6 的 Docker 网络:
docker network create \
--driver bridge \
--ipv6 \
--subnet fd00:3939::/64 \
caddy
再把容器加入到这个新建的 caddy
网络即可。
文件访问
在 Nginx 中通常会加上这么个配置:
location ~ /.well-known {
allow all;
}
location ~ /\. {
deny all;
}
前者用于允许路径 /.well-known
下内容的访问1,后者则是禁止访问以 点号开头的隐藏文件(比如 .git
、.htaccess
、.env
等)。
Caddy 默认会对 /.well-known/acme-challenge
这类路径自动处理,而对于以点开头的隐藏文件,使用 @hiddenFiles
匹配所有路径以 /
+ 点号 开头的请求,再 respond
返回 HTTP 403 禁止访问即可。
@hiddenFiles {
path_regexp hiddenFiles ^/\..*
}
respond @hiddenFiles 403
塞到网站配置中就能用了:
example.com {
encode zstd gzip
root * /srv/blog
file_server
@hiddenFiles {
path_regexp hiddenFiles ^/\..*
}
respond @hiddenFiles 403
}
- 这是一个标准路径,通常用来放置一些公开的、需要被外部访问的文件或信息,比如 Let's Encrypt 证书验证文件(ACME Challenge)、安全政策文件(如
security.txt
)、其他协议定义的公开信息文件。 ↩
本文作者:mikusa
本文链接:https://www.himiku.com/archives/something-about-caddy.html
版权声明:所有文章除特别声明外均系本人自主创作,转载及引用请联系作者,并注明出处(作者、原文链接等)。
好复杂!
我今年也把原来dnmp中的nginx换成了caddy,但是完全没有深入配置,能简单跑起来就行……就图个能把证书申请部署的步骤去掉。看来之后得再研究研究。
我是照着我 Nginx 的配置问的 AI,然后才水出来这些的
就是 AI 有时候答 Caddy v1,有时候答 JSON,搞得我头大
其实大部分配置都没啥
用,更多是我的强迫症。就是出于安全考虑,隐藏文件 403 掉还是有点必要的。反正我是坚定的Apache不动摇,
.htaccess
真的太香了。新潮软件更喜欢用JSON当日志也不是没有道理,因为这样写日志分析工具简单多了,相比CLF需要手写正则才能匹配内容甚至还得防着日志分析工具被注入的大坑,JSON还是太好用了。当然反正不论是JSON还是CLF都没什么人认真正经写日志分析工具。
后台看不到正确的IPv6地址这个不应该算作IPv6的锅,应该算Docker的。
可惜我一开始搭博客就是学的lnmp,换到docker也是dnmp,apache完全没接触过……

IPv6我一开始完全没想到是 Docker的,直到我注意到地址都是 172.18.0.2……
也是被国内大厂拐坏了,HTTP引擎就应该拿来就用。
但是国内大厂就是把风气搞得极臭,各种魔改配置的nginx和tomcat,甚至还有分岔出来的超多大坑的Tengine都被一堆人捧臭脚,然后编造各种理由疯狂诋毁Apache,堪比网暴。
性能上和nginx有区别吗?配置起来看着好像更复杂一点
性能可能高并发下干不过 nginx,但配置肯定是比 nginx 简单的。就是再加上 php 的话,这玩意内存占用要比 nginx 高,纯反代用会比较好点……