众所周知 本博客始建于今年(2025)年初,当时由于是第一次涉足博客系统的搭建,所以主要的目标是一切从简,并且保证博客系统的轻量化与可维护性。但是在近半年的使用下来,我还是发现了原版的 Vanblog 一些不容忽视的问题:
基于以上几点原因,我转向了 Vanblog 交流群中另外一位开发者的二开项目,由于是开发者自己使用所以更新很积极,并且配置文件兼容原版的 Vanblog ,也修复了很多原版的Bug、添加了大量的自定义功能。最重要的是,这个二开项目可以实现 Caddy 服务和 Vanblog 系统服务的分离,方便对 Caddy 进行更多个性化配置。
由于之前仅使用 FRP 来实现 HTTP 流量的转发,所以我是直接在 FRPs 的配置文件中将 80 和 443 配置给 HTTP 和 HTTPS 协议的。
ini展开代码vhost_http_port = 80
vhost_https_port = 443
这个操作实质上是将 FRPs 的 HTTP/HTTPS 协议端口直接暴露到公网,这本身其实并没有太大的问题,因为FRP本身就是为了反向代理而制作的,但是由于 FRP 仅处理 HTTP/HTTPS 协议流量的转发,而不处理 TLS 证书的验证,所以这就导致我必须在本地服务器上为每个服务单独配置好 TLS 证书验证,其整体系统框架大致如下:
graph TD
subgraph "外网"
A[用户请求]
B(FRPS<br/>转发HTTP/HTTPS/TCP流量)
end
subgraph "内网"
C(FRPC<br/>客户端内网服务)
D(Vanblog系统容器)
E(其他的内网服务……)
end
A --"HTTP/HTTPS"--> B
B -- "TCP/UDP" --> C
C --> D
C --> E
style B fill:#ff9,stroke:#333,stroke-width:2px;
style C fill:#ff9,stroke:#333,stroke-width:2px;
我个人觉得从逻辑上来讲这种操作实在是本末倒置,所以就萌生了将 Caddy 服务转移到公网服务器,专门进行 TLS 证书相关处理的想法。
从流程上来看就是在FRPs服务的前面再套一个Caddy来实现。虽然从理论上讲这样套了两层的流量转发可能会导致一些性能损失,但对于博客网站这点流量来说根本不会产生任何可感知的影响,相对的,在全域实现HTTPS访问后用户与公网之间的流量安全性会大幅提高,像是 Cloudreve 个人网盘中的文件流量也可以安全无虞地进行传输(虽然其实也没什么东西awa)。
综上改进后的整体系统框架大致如下:
graph TD
subgraph "外网"
A[用户请求] --"HTTPS"--> B(Caddy<br/>处理TLS证书<br/>HTTP自动重定向<br/>反向代理);
B --"HTTP"--> C(FRPS<br/>转发HTTP/HTTPS/TCP流量);
end
subgraph "内网"
D(FRPC<br/>客户端内网服务) --> E(Vanblog系统容器);
D --> F(其他的内网服务……)
end
style C fill:#ff9,stroke:#333,stroke-width:2px;
style D fill:#ff9,stroke:#333,stroke-width:2px;
linkStyle 2 stroke:#000,stroke-width:2px;
C -- "TCP/UDP" --> D;
虽然经过前文的分析,整个网站框架不过是在最前面安装一个 Caddy 服务,但是分解到具体步骤还是有不少需要注意的点。像是 Caddy 版本和插件的选择、Caddyfile 的编写、caddy.service 系统服务脚本的编写等等。下面我将按步骤详细阐述。
这里我没有选择官方编译好的的原版 Caddy2 ,而是下载源码后使用 go 在本地编译构建。这是由于在后面的TLS证书获取步骤中,需要进行 DNS-01 网站所有权验证,而我刚好使用的是阿里云的域名解析服务,所以可以通过安装dns.providers.alidns
插件,通过为其配置阿里云RAM账户的AccessKey来实现自动所有权验证。
注
以下操作均在安装了 Debian 操作系统的服务器上进行。
bash展开代码# 确保已安装 go(1.21+),然后安装 xcaddy:
sudo apt update
sudo apt install -y golang-go git build-essential # 如果没装 go
export PATH=$PATH:$(go env GOPATH)/bin
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
这边如果安装的 go 版本太低就可能会在后面go install
这一步提示unknown directive: toolchain
bash展开代码go: github.com/caddyserver/xcaddy/cmd/xcaddy@latest (in github.com/caddyserver/xcaddy@v0.4.5): go.mod:5: unknown directive: toolchain
如果下载速度太慢,可以自行更改go的下载源,网上教程很多,这里不再赘述。
bash展开代码# 在 /usr/local/bin 输出 caddy(二进制路径可按需改)
xcaddy build --with github.com/caddy-dns/alidns@latest --output /usr/local/bin/caddy
# 检查
/usr/local/bin/caddy version
整个构建过程会比较慢,我的服务器(双核2G)大概跑了 5min 左右才完事。等待构建完毕后就应当能在前面配置的输出文件夹中看到编译好的 Caddy 二进制文件了。
bash展开代码# 检查输出文件夹下是否有编译好的二进制文件
ls -l /usr/local/bin/caddy
# 输出类似
# -rwxr-xr-x 1 root root 12345678 Aug 13 20:35 /usr/local/bin/caddy
# 确认 Caddy 版本:
/usr/local/bin/caddy version
# 输出类似
# v2.10.0 h1:fonub……
# 确保插件已被包含:
/usr/local/bin/caddy list-modules | grep alidns
# 输出类似
# dns.providers.alidns
确保编译好的 Caddy 版本和插件无误后,编写caddy.serviec
服务脚本并将其放到/etc/systemd/system/
文件夹下,并赋予执行权限。
caddy.serviec
服务脚本内容如下:
ini展开代码[Unit]
Description=Caddy v2 web server
Documentation=https://caddyserver.com/docs/
After=network-online.target
Wants=network-online.target
[Service]
# 以非特权用户运行(如果不存在,请先创建 caddy 用户)
User=caddy
Group=caddy
# 通过 EnvironmentFile 载入敏感配置(如 ALIYUN_ACCESS_KEY_*)
EnvironmentFile=/etc/caddy/secret.env
# 确保在启动前校验配置(若校验失败,systemd 会拒绝启动)
ExecStartPre=/usr/local/bin/caddy validate --config /etc/caddy/Caddyfile
# 主进程:运行 caddy(--environ 会在日志显示环境变量,便于调试;需要时可去掉)
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile --adapter caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
# 关闭
TimeoutStopSec=5s
KillMode=process
# 重启策略
Restart=on-failure
RestartSec=5s
# 文件句柄上限
LimitNOFILE=1048576
# 允许的能力(绑定 80/443)
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 保护手段(更严格的可根据需要调整)
ProtectSystem=full
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=true
# 只允许对这些路径读写(便于限制范围)
ReadWriteDirectories=/etc/caddy /var/lib/caddy /var/log/caddy
ReadOnlyDirectories=/usr /bin /lib /usr/local
# 运行时目录,systemd 会创建并设权限
RuntimeDirectory=caddy
RuntimeDirectoryMode=0750
# 能提高安全的可选项(按需取消注释)
# ProtectKernelTunables=yes
# ProtectKernelModules=yes
# RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target
这边需要注意的有三点:
Caddyfile
不在默认位置 /etc/caddy/Caddyfile
,那么你需要在 .service 文件中明确指定配置文件的路径。这样做可以确保 systemd 在启动 Caddy 时,能够找到并使用你指定的 Caddyfile
;ExecStartPre=
、ExecStart=
、ExecReload=
这几个地方后面对应的 /usr/local/bin/caddy
这个地址应当指向你前面编译Caddy后输出的可执行文件的地址;EnvironmentFile=/etc/caddy/secret.env
这一行是用来配置插件的环境变量的,其中的 secret.env
则是对应保存了阿里云RAM账户 AccessKeyID
和 AccessKeySecret
的文件,这个文件存放的位置随意,其内容编写详见下文。dns.providers.alidns
插件环境变量配置这里我推荐使用 .env 文件方式来进行配置,其好处是可以通过配置环境变量的方式,免于将 RAM 账户高敏感度信息直接记录在 Caddyfile
配置文件中,并且通过配置 .env 文件的所有权可以防止其他账户或应用读取和修改其中内容。
如上文所述,我将secret.env
文件放置在/etc/caddy/
这个文件夹下,并设置其所有权给 caddy用户
和 caddy用户组
。
bash展开代码# 设置文件所有权和读写权限
sudo chown caddy:caddy /etc/caddy/secret.env
sudo chmod 600 /etc/caddy/secret.env
secret.env
中的内容如下:
ini展开代码ALIYUN_ACCESS_KEY_ID=your_key_id
ALIYUN_ACCESS_KEY_SECRET=your_key_secret
关于阿里云 RAM 账户的配置和 AccessKey 的获取可以详见我的这篇文章的后半部分。
Caddyfile
配置文件Caddyfile
文件默认应保存在 /etc/caddy/
目录下:
ini展开代码polaristation.fun {
# 配置主域名网站的本地目录地址,我在这个目录下放置了一个 index.html 作为导航页
root * /usr/local/caddy/polaristation.fun
# Enable the static file server.
file_server
# Another common task is to set up a
# reverse proxy: reverse_proxy localhost:8080
# Or serve a PHP site through php-fpm:
# php_fastcgi localhost:9000
}
*.polaristation.fun {
# 在这里为所有域名配置通用的 TLS 功能
tls {
# 使用 alidns 插件进行 DNS 域名所有权验证
dns alidns {
# 加载 secret.env 文件中对应的环境变量
access_key_id {env.ALIYUN_ACCESS_KEY_ID}
access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET}
}
}
# 这里配置的反向代理端口xxxx必须和服务器上
# FRPs配置文件中的vhost_http_port所配置的端口一致
reverse_proxy 127.0.0.1:xxxx {
header_up X-Real-IP {http.request.remote}
header_up X-Forwarded-For {http.request.remote}
header_up X-Forwarded-Port {http.request.port}
header_up X-Forwarded-Proto {http.request.scheme}
}
}
# 下面是具体的域名配置块,前面已经配置过泛域名证书的获取,
# Caddy 会自动匹配其他子域名无需重复 tls{}
xxx.polaristation.fun {
reverse_proxy 127.0.0.1:xxxx
}
#blog.polaristation.fun {
# reverse_proxy 127.0.0.1:xxxx
#}
#
#cloud.polaristation.fun {
# reverse_proxy 127.0.0.1:xxxx
#}
#
前述几个准备工作完成以后,就可以直接使用 systemctl
的命令通过服务脚本启动 Caddy 服务了。
bash展开代码# 如果之前被 masked,先 unmask
sudo systemctl unmask caddy.service
# 重新加载 systemd 单元文件
sudo systemctl daemon-reload
# 启用并启动服务
sudo systemctl enable --now caddy.service
# 检查状态与日志(实时)
sudo systemctl status caddy.service --no-pager
sudo journalctl -u caddy.service -f
如果启动失败,请先使用 sudo /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile
手动验证 Caddyfile
的语法,查看错误提示再修正。
如果一切正常那么此时访问 https://polaristation.fun 后就应当会直接显示之前保存在 /usr/local/caddy/polaristation.fun
目录下的 index.html
网页
FRPs 服务配置很简单,参考官方的文档或者网上教程来就行,这边我直接把frps.toml
文件内容贴出来(敏感信息已隐藏,根据自己情况修改),以供参考:
toml展开代码
bindAddr = "0.0.0.0"
bindPort = 7000
# udp port used for kcp protocol, it can be same with 'bindPort'.
# if not set, kcp is disabled in frps.
kcpBindPort = 7001
# udp port used for quic protocol.
# if not set, quic is disabled in frps.
quicBindPort = 7002
# Specify which address proxy will listen for, default value is same with bindAddr
# proxyBindAddr = "127.0.0.1"
# quic protocol options
# transport.quic.keepalivePeriod = 10
# transport.quic.maxIdleTimeout = 30
# transport.quic.maxIncomingStreams = 100000
# Heartbeat configure, it's not recommended to modify the default value
# The default value of heartbeatTimeout is 90. Set negative value to disable it.
transport.heartbeatTimeout = 90
# Pool count in each proxy will keep no more than maxPoolCount.
transport.maxPoolCount = 10
# If tcp stream multiplexing is used, default is true
transport.tcpMux = true
# Specify keep alive interval for tcp mux.
# only valid if tcpMux is true.
# transport.tcpMuxKeepaliveInterval = 30
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# transport.tcpKeepalive = 7200
# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
# transport.tls.force = false
# transport.tls.certFile = "server.crt"
# transport.tls.keyFile = "server.key"
# transport.tls.trustedCaFile = "ca.crt"
# If you want to support virtual host, you must set the http port for listening (optional)
# Note: http port and https port can be same with bindPort
vhostHTTPPort = xxxx
vhostHTTPSPort = xxxx
# Response header timeout(seconds) for vhost http server, default is 60s
# vhostHTTPTimeout = 60
# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
# requests on one single port. If it's not - it will listen on this value for
# HTTP CONNECT requests. By default, this value is 0.
# tcpmuxHTTPConnectPort = 1337
# If tcpmuxPassthrough is true, frps won't do any update on traffic.
# tcpmuxPassthrough = false
# Configure the web server to enable the dashboard for frps.
# dashboard is available only if webServerport is set.
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "your_usrname"
webServer.password = "your_password"
# webServer.tls.certFile = "server.crt"
# webServer.tls.keyFile = "server.key"
# dashboard assets directory(only for debug mode)
# webServer.assetsDir = "./static"
# Enable golang pprof handlers in dashboard listener.
# Dashboard port must be set first
# webServer.pprofEnable = false
# enablePrometheus will export prometheus metrics on webServer in /metrics api.
# enablePrometheus = true
# console or real logFile path like ./frps.log
log.to = "enable"
# trace, debug, info, warn, error
log.level = "info"
log.maxDays = 7
# disable log colors when log.to is console, default is false
# log.disablePrintColor = false
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
# detailedErrorsToClient = true
# auth.method specifies what authentication method to use authenticate frpc with frps.
# If "token" is specified - token will be read into login message.
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
auth.method = "token"
# auth.additionalScopes specifies additional scopes to include authentication information.
# Optional values are HeartBeats, NewWorkConns.
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
# auth token
auth.token = "your_token"
# userConnTimeout specifies the maximum time to wait for a work connection.
# userConnTimeout = 10
# Max ports can be used for each client, default value is 0 means no limit
# maxPortsPerClient = 0
# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
# When subdomain is test, the host used by routing is test.frps.com
subDomainHost = "polaristation.fun"
# custom 404 page for HTTP requests
# custom404Page = "/path/to/404.html"
# specify udp packet size, unit is byte. If not set, the default value is 1500.
# This parameter should be same between client and server.
# It affects the udp and sudp proxy.
# udpPacketSize = 1500
# Retention time for NAT hole punching strategy data.
# natholeAnalysisDataReserveHours = 168
# ssh tunnel gateway
# If you want to enable this feature, the bindPort parameter is required, while others are optional.
# By default, this feature is disabled. It will be enabled if bindPort is greater than 0.
# sshTunnelGateway.bindPort = 2200
# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa"
# sshTunnelGateway.autoGenPrivateKeyPath = ""
# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys"
因为这边我设置了 subDomainHost = "polaristation.fun"
,所以在 FRPc 端只需设置 subdomain = "blog"
即可,而无需配置完整 customdomain
本地所需配置的服务非常简单,仅有一个 FRPc 需要进行设置,用于将本地服务端口的流量转发到公网服务器的对应端口上。
FRPc 相关配置文件内容如下:
ini展开代码[common]
log_level = info
admin_port = 7500
admin_user = your_usrname
admin_pwd = your_password
tls_enable = false
token = your_token
server_addr = your_server_addr
http_proxy = 16337
protocol = tcp
server_port = 7000
[Blog]
type = http
use_encryption = true
use_compression = true
local_ip = 127.0.0.1
local_port = xxxx
subdomain = blog
[Cloudreve]
type = http
use_encryption = true
use_compression = true
local_ip = 127.0.0.1
local_port = xxxx
subdomain = cloud
通过以上服务端与客户端的配合,即可实现通过 FRPc 服务将本地服务端口流量转发到公网服务器 FRPs 服务所设置的 vhostHTTPPort
端口上,再通过 Caddyfile
中 *.polaristation.fun{reverse_proxy 127.0.0.1:xxxx {…}}
这一项配置实现通过子域名对流量自动匹配进行反向代理。
本文作者:Polaris⭐
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 Polari_S_tation 版权所有 许可协议。转载请注明出处!