博客系统架构小幅改动
2025-08-14
网络
00

目录

进度
前言
整体博客系统的架构调整
公网服务器相关服务部署
Caddy服务的安装
安装 xcaddy(用它来构建带插件的 Caddy)
用 xcaddy 构建包含 alidns 插件的 Caddy
配置 Caddy 的系统服务脚本
dns.providers.alidns插件环境变量配置
编写 Caddyfile 配置文件
尝试启动 Caddy 服务
FRPs 服务配置
本地服务器相关服务部署
小结

进度

  • 公网服务器部署 Caddy 服务并自动获取泛域名TLS证书
  • 部署双端 FRP 服务互联实现内网穿透
  • 全站完全实现 HTTPS 访问

前言

众所周知 本博客始建于今年(2025)年初,当时由于是第一次涉足博客系统的搭建,所以主要的目标是一切从简,并且保证博客系统的轻量化与可维护性。但是在近半年的使用下来,我还是发现了原版的 Vanblog 一些不容忽视的问题:

  • 由于原作者已经长时间未更新,导致目前的最新版本仍然有一些较为严重的 Bug(如文章编辑页面 Mermaid 流图渲染错误导致崩溃)无法修复,而本人并非专攻于网页前端开发,所以对于修复这类Bug实在无能为力;
  • 原版 Vanblog 的 HTTPS 证书自动获取服务利用内置的 Caddy 服务实现,所以只能部署在与 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 系统服务脚本的编写等等。下面我将按步骤详细阐述。

Caddy服务的安装

这里我没有选择官方编译好的的原版 Caddy2 ,而是下载源码后使用 go 在本地编译构建。这是由于在后面的TLS证书获取步骤中,需要进行 DNS-01 网站所有权验证,而我刚好使用的是阿里云的域名解析服务,所以可以通过安装dns.providers.alidns插件,通过为其配置阿里云RAM账户的AccessKey来实现自动所有权验证。

以下操作均在安装了 Debian 操作系统的服务器上进行。

安装 xcaddy(用它来构建带插件的 Caddy)

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的下载源,网上教程很多,这里不再赘述。

用 xcaddy 构建包含 alidns 插件的 Caddy

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 版本和插件无误后,编写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

这边需要注意的有三点:

  • 在使用 systemd 来管理 Caddy 服务时,如果你的 Caddyfile 不在默认位置 /etc/caddy/Caddyfile,那么你需要在 .service 文件中明确指定配置文件的路径。这样做可以确保 systemd 在启动 Caddy 时,能够找到并使用你指定的 Caddyfile
  • ExecStartPre=ExecStart=ExecReload= 这几个地方后面对应的 /usr/local/bin/caddy 这个地址应当指向你前面编译Caddy后输出的可执行文件的地址;
  • EnvironmentFile=/etc/caddy/secret.env 这一行是用来配置插件的环境变量的,其中的 secret.env 则是对应保存了阿里云RAM账户 AccessKeyIDAccessKeySecret 的文件,这个文件存放的位置随意,其内容编写详见下文。

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 #} #

尝试启动 Caddy 服务

前述几个准备工作完成以后,就可以直接使用 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 服务配置很简单,参考官方的文档或者网上教程来就行,这边我直接把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 版权所有 许可协议。转载请注明出处!