结合了 FRP 的内网穿透能力和 Caddy 强大的 Web 服务管理能力。这个组合可以让你安全、高效地将家庭内网的多个服务暴露到公网。
主要功能:
Caddy Auto HTTPS 功能默认使用 ACME 协议的 HTTP-01
或 TLS-ALPN-01
质询(Challenge)来向 Let's Encrypt 等证书颁发机构(CA)证明它确实控制着这个域名。
http://your.domain.com/.well-known/acme-challenge/some-random-token
)。你的 Caddy 服务器必须能响应这个请求。abitacc.com
解析到云服务器 (ECS) 的公网 IP。http://abitacc.com
或连接其 443 端口时,它连接到的是你的云服务器,而不是你内网设备上的 Caddy服务。简单来说,HTTP-01
验证要求从公网的 80 端口能够直接、无障碍地访问到发起证书申请的那个 Caddy 服务。FRP 这层隧道破坏了这种“直接性”。因此,Caddy 的标准自动 HTTPS 流程会失败,因为Caddy无法向外界证明自己对域名的所有权。
最优的解决方法是采用DNS-01 质询。
由于FRP可以转发TCP,因此可以把80或443的访问直接转发到内网的Caddy服务,从而也能够完成HTTP-01质询
工作流程:
80
端口。frps
收到这个 TCP 连接,不解析内容,直接通过隧道将原始的 TCP 数据流转发给 frpc
。frpc
将这个 TCP 数据流转发给本地 Caddy 的 80
端口。HTTP-01
验证。优点:
HTTP-01
验证的问题。缺点(这也是为什么它不被普遍推荐):
vhost
(虚拟主机) 功能是基于 type = http
的。一旦你把 80
和 443
端口用 type = tcp
模式“独占”了,frps
就失去了作为应用层反向代理的能力。它无法再根据域名 (Host
header) 来决定把流量转发给哪个 frpc
客户端。80
和 443
端口被完全绑定给了这一个 FRP 隧道。如果你想把另一个域名 another.domain.com
指向另一台家庭设备上的另一个 frpc
,你做不到了,因为 80/443
端口已经被占用了。优点:
frps
可以在 80/443
端口上接收任意数量的域名请求,并根据 customDomains
的配置,将流量智能地分发到位于不同物理位置、不同内网的多个 frpc
客户端。frps
负责公网入口和流量分发;frpc
负责建立隧道;内网的 Caddy/Nginx 等负责具体的服务代理和证书管理。职责分明。a.domain.com
指向家里的 NAS,明天就可以轻松增加一个 b.domain.com
指向办公室的开发服务器,它们可以共享同一个 frps
服务。缺点:
DNS-01
质询需要额外的一步(获取 API Token),比 TCP
转发稍微复杂一些。TCP
转发可以让 HTTP-01
质询工作。但这样做会牺牲掉 FRP 最强大的特性之一——应用层(HTTP)的虚拟主机路由能力,导致整个架构变得僵化和难以扩展。
因此,在绝大多数场景下,HTTP
转发 + DNS-01
质询 是一个远比 TCP
转发 + HTTP-01
质询 更优秀、更灵活、更具扩展性的架构方案。它保留了 FRP 和 Caddy 各自最大的优势。
docker pull caddy:2.10.0-builder
docker pull caddy:2.10.0-alpine
caddy-dns-manager
),并勾选 OpenAPI 调用访问。点击确定。AccessKey Secret
,这个页面关掉后就再也看不到了。AliyunDNSFullAccess
(管理云解析(DNS)的权限),然后确定。这样,Caddy 能够拥有操作域名解析API的“钥匙”。
frp
配置简单frp
配置不需要任何复杂的东西,就用最基础的 HTTP
转发。
frps.toml
bindPort = 7000
vhostHttpPort = 80
vhostHTTPSPort = 443
token = your_secure_token
vhost_http_port = 80
: 直接监听 80 端口,接收所有 HTTP 流量。frpc.toml
serverAddr = your_vps_public_ip
serverPort = 7000
token = your_secure_token
[[proxies]]
name = "caddy_http"
type = http
local_port = 80
customDomains = ["your.domain.com", "a.your.domain.com", "b.your.domain.com"]
[[proxies]]
name = "caddy_https"
type = https
local_port = 443
customDomains = ["your.domain.com", "a.your.domain.com", "b.your.domain.com"]
customDomains
: 列出所有你需要穿透的域名。local_port = 80
: frpc
会把流量转发给本地的 80 端口,Caddy 将在这里监听。Dockerfile的内容如下:
FROM caddy:2.10.0-builder AS builder
# ARG http_proxy
# ARG https_proxy
# ENV http_proxy=$http_proxy
# ENV https_proxy=$https_proxy
# ENV GOPROXY=https://goproxy.cn
RUN xcaddy build --with github.com/caddy-dns/alidns
FROM caddy:2.10.0-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
运行 docker build -t caddy:2.10.0-aliyundns .
来创建镜像,并将 docker-compose.yml
中的 image
改为 caddy:2.10.0-aliyundns
。
使用dockge编写compose.yaml,内容如下:
services:
caddy:
container_name: caddy
image: caddy:2.10.0-aliyundns
restart: unless-stopped
hostname: caddy
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- $PWD/config:/etc/caddy
- data_vol:/data
- conf_vol:/config
cap_add:
- NET_ADMIN
environment:
- ALIYUN_ACCESS_KEY_ID="Your AccessKeyID"
- ALIYUN_ACCESS_KEY_SECRET="Your AccessKeySecret"
networks:
default:
name: caddy_default
volumes:
data_vol:
name: caddy_data
conf_vol:
name: caddy_conf
注意:Do not mount the Caddyfile directly at /etc/caddy/Caddyfile
, If vim or another editor is used that changes the inode of the edited file, the changes will only be applied within the container when the container is recreated, which is explained in detail in this Medium article. When using such an editor, Caddy's graceful reload functionality might not work as expected, as described in this issue.
编写Caddyfile,内容如下:
{
acme_dns alidns {
access_key_id {env.ALIYUN_ACCESS_KEY_ID}
access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET}
}
}
gerrit.abitacc.com {
reverse_proxy 192.168.0.110:8088
}
在dockge web界面启动caddy,查看dockge终端里面有没有错误日志。
错误日志:
custom domain [gerrit.abitacc.com] should not belong to subdomain host [abitacc.com]
frpc
客户端尝试注册一个自定义域名 gerrit.abitacc.com
,但是这个域名看起来像是我(frps
)被配置用来管理子域名的基础域名(subdomain_host
)的子域名。这两种模式是冲突的,不允许这样做。
FRP 有两种处理域名的方式,只能选择其中一种:
子域名模式 (subdomain_host
): 在 frps
中设置一个基础域名(如 subdomainHost = abitacc.com
)。然后 frpc
客户端可以非常方便地通过设置 subdomain = gerrit
来自动创建 gerrit.abitacc.com
。这是为了快速、便捷地创建多个子域名。
自定义域名模式 (customDomains
): 不在 frps
中设置任何基础域名。frps
作为一个通用的代理,frpc
客户端通过 customDomains
参数明确地告诉 frps
它要负责哪些完整的、独立的域名。这是更灵活、更通用的方式。
当在 frps
中设置了 subdomainHost = abitacc.com
,又在 frpc
中使用 customDomains = gerrit.abitacc.com
时,frps
就陷入了混乱。它不知道应该按哪种模式来处理这个请求,为了避免歧义,它直接拒绝了这种代理的注册。
curl -vL -k https://gerrit.abitacc.com
-v
(verbose): 显示详细的连接过程,包括 DNS 解析、TCP 连接、TLS 握手和 HTTP 头信息。-L
(location): 自动跟随重定向。-k
(insecure): 允许不安全的连接错误日志:
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: CN=gerrit.abitacc.com
* start date: Jul 12 07:43:38 2025 GMT
* expire date: Oct 10 07:43:37 2025 GMT
* issuer: C=US; O=Let's Encrypt; CN=E6
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x56083d988dc0)
> GET / HTTP/2
> Host: gerrit.abitacc.com
> user-agent: curl/7.68.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 502
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Sat, 12 Jul 2025 09:13:00 GMT
<
* Connection #0 to host gerrit.abitacc.com left intact
从日志可以看出:
Connected to gerrit.abitacc.com (47.97.122.181)
表示您的 frps
和 frpc
之间的域名路由配置完全正确。SSL certificate verify ok.
和 subject: CN=gerrit.abitacc.com
证明了您的 Caddy 已经成功获取并提供了正确的 HTTPS 证书。整个链路 User -> frps -> frpc -> Caddy
已经打通。现在遇到的问题是经典的 502 Bad Gateway
错误。
排查定位:
curl -vL http://localhost:8088
HTTP/1.1 200 OK
意味着本地连接和服务响应都没有任何问题。
sudo netstat -tulnp | grep 8088
检查 服务是否在监听正确的端口和地址
由于Caddy是运行在Docker容器里面,其Docker网络是与Host网络隔离的。在容器内部,localhost
或 127.0.0.1
指的是容器本身,而不是运行 Docker 的主机。Caddy 的配置 reverse_proxy localhost:8088
让 Caddy 尝试连接容器自己的 8088
端口。
Docker 为此提供了一个特殊的 DNS 名称:host.docker.internal
。host.docker.internal
是一个比较新的特性,在某些旧版本的 Docker 或者特定的 Linux 发行版上可能默认不被支持。因此把reverse_proxy localhost:8088
改成host.docker.internal:8088
也不一定能够起作用。
注意:修改Caddyfile,需要restart caddy docker container
最后尝试将reverse_proxy localhost:8088
改成reverse_proxy 192.168.0.110:8088
。
localhost
不行: 因为 Caddy 在 Docker 容器内,localhost
指向容器自身,而不是主机。host.docker.internal
不行: 这说明在您的 Docker 环境中,这个特殊的 DNS 名称没有被正确解析或路由到主机。这在某些 Linux 发行版或较旧的 Docker 版本上是常见情况。192.168.0.110
(主机的局域网 IP) Okay: 证明:filebrowser
服务在主机的 192.168.0.110
上正监听 8088
端口。ufw
是 inactive
的)。最终,使用主机的局域网 IP 地址,是连接容器内服务和主机上服务的“黄金标准”,它绕过了所有特定于平台的 DNS 解析问题,是最稳定、最可靠的方法。
错误日志:
package main
import (
caddycmd "github.com/caddyserver/caddy/v2/cmd"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/caddy-dns/alidns"
)
func main() {
caddycmd.Main()
}
2025/07/11 16:47:56 [INFO] Initializing Go module
2025/07/11 16:47:56 [INFO] exec (timeout=0s): /usr/local/go/bin/go mod init caddy
go: creating new go.mod: module caddy
go: to add module requirements and sums:
go mod tidy
2025/07/11 16:47:56 [INFO] Pinning versions
2025/07/11 16:47:56 [INFO] exec (timeout=0s): /usr/local/go/bin/go get -v github.com/caddyserver/caddy/v2@v2.10.0
go: github.com/caddyserver/caddy/v2@v2.10.0: Get "https://proxy.golang.org/github.com/caddyserver/caddy/v2/@v/v2.10.0.info": proxyconnect tcp: EOF
2025/07/11 16:47:56 [FATAL] exit status 1
The command '/bin/sh -c xcaddy build --with github.com/caddy-dns/alidns' returned a non-zero code: 1
在中国大陆,直接访问 proxy.golang.org
可能会很慢或不稳定。可以在构建时额外设置 GOPROXY
环境变量,将其指向一个国内的镜像源,这会大大加快构建速度。
因此要配置docker build代理。Docker build的代理有两种配置方法:
docker build
命令中通过 --build-arg
传入代理设置Dockerfile添加如下内容:
ARG http_proxy
ARG https_proxy
ENV http_proxy=$http_proxy
ENV https_proxy=$https_proxy
# 设置 Go 模块代理为国内镜像
ENV GOPROXY=https://goproxy.cn,direct
运行docker build --build-arg httpproxy="http://192.168.0.110:7890" --build-arg https_proxy="http://192.168.0.110:7890" -t caddy:2.10.0-aliyundns
。如果没有使用--build-arg
代入代理配置,Dockerfile
内部的 RUN
命令并不会通过代理进行访问网络和下载资源。
~/.docker/config.json
){
"proxies": {
"default": {
"httpProxy": "http://192.168.0.110:7890",
"httpsProxy": "http://192.168.0.110:7890",
"noProxy": "localhost,127.0.0.0"
}
}
}
如果两者都配置,优先使用~/.docker/config.json
的配置。即使config.json配置错误,也不会生效--build-arg
配置。
排查过程的尝试:
1. 在构建时临时让 Docker 容器直接使用主机的网络,而不是它自己的隔离网络,修改您的 docker build
命令,加入 --network=host
参数。
2. 开关clash Allow LAN选项
Allow LAN
= ON: Clash 会监听 0.0.0.0:7890
。这意味着它准备好接收来自任何网络接口(包括局域网 192.168.0.x
和 容器 172.17.0.x
)的连接。Allow LAN
= OFF: Clash 只会监听 127.0.0.1:7890
。这是一个安全设置,意味着只有在 192.168.0.110
这台机器上运行的程序才能连接到代理。RootCause分析:
~/.docker/config.json
里面错误配置"httpsProxy": "https://192.168.0.110:7890"
,clash不支持https代理协议错误日志:
caddy | Error: adapting config using caddyfile: parsing caddyfile tokens for 'acme_dns': AccessKeyID or AccessKeySecret is empty, at /etc/
解决办法:
compose.yaml添加下面两个变量:
environment:
- ALIYUN_ACCESS_KEY_ID=YourAccessKeyID
- ALIYUN_ACCESS_KEY_SECRET=YourAccessKeySecret
Caddyfile里面需要引用这两个变量
{
acme_dns alidns {
access_key_id {env.ALIYUN_ACCESS_KEY_ID}
access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET}
}
}
错误日志:
caddy | {"level":"error","ts":1752309447.0892036,"logger":"tls.obtain","msg":"will retry","error":"[gerrit.abitacc.com] Obtain: [gerrit.abitacc.com] solving challenges: presenting for challenge: adding temporary record for zone \"abitacc.com.\": get error status: HTTP 403: User not authorized to operate on the specified resource, or this API doesn't support RAM. (order=https://acme-v02.api.letsencrypt.org/acme/order/2523960391/405665995491) (ca=https://acme-v02.api.letsencrypt.org/directory)","attempt":1,"retrying_in":60,"elapsed":4.232440452,"max_duration":2592000}
从日志看出:
HTTP 403: User not authorized to operate on the specified resource
错误原因:
这个 403 Forbidden
错误来自阿里云的 API。它表示您在 docker-compose.yml
中提供的 AccessKeyID
和 AccessKeySecret
所对应的 RAM 用户,没有足够的权限 来修改 abitacc.com
这个域名的 DNS 记录。Caddy 为了证明您拥有这个域名,需要通过阿里云的 API 临时添加一条 DNS TXT 记录。阿里云拒绝了这个操作,因为您的密钥权限不足。
解决方案:
需要登录到阿里云的 RAM 访问控制 控制台,给使用的 AccessKey 所属的 RAM 用户授予管理 DNS 的权限,在 选择权限 步骤中,搜索并选择 AliyunDNSFullAccess
这个系统策略。这是最简单的方法,它授予了管理所有 DNS 的完整权限。注意:千万不要选择错误