打开网易新闻 查看精彩图片

一台服务器上跑两个Caddy,就像一间公寓里住了两个房东,都觉得自己该收房租。Mercure自带的嵌入式Caddy非要占443端口,可你主Caddy早就在那儿了。冲突不是"可能",是一定。

这是部署Mercure(一种基于服务器推送事件/SSE的实时协议)时最隐蔽的坑。很多人按官方文档装完,发现服务起不来,日志里全是端口占用。本文用Ansible完整复现一套共存方案,包括三个你Google不到的细节。

第一步:把Mercure的Caddy关进笼子

第一步:把Mercure的Caddy关进笼子

Mercure的嵌入式Caddy默认行为很霸道:自动申请HTTPS证书、开2019端口管自己、绑定443对外服务。这些必须全关。

配置块只有三行,缺一行都炸:

auto_https off —— 阻止它去Let's Encrypt要证书,你的主Caddy会处理TLS。

admin localhost:2039 —— 把管理API从2019挪走,主Caddy的admin还占着2019。

http://localhost:3080 —— 只监听本地回环,外网流量由主Caddy反向代理进来。

打开网易新闻 查看精彩图片

端口3080是作者踩坑后的建议。3000-3099这个区间Docker爱用,但3080相对冷门。你可以netstat看一眼再定,重点是别跟现有服务撞车。

第二步:主Caddy的反向代理有致命细节

第二步:主Caddy的反向代理有致命细节

主Caddy配置看起来标准,但有两个参数写错就全完。

flush_interval -1 是SSE的生命线。 Caddy默认会缓冲响应,等攒够一批再发。可SSE(服务器推送事件)是流,永远等不到"一批"。不设-1,客户端永远收不到消息,调试时你会怀疑人生——连接成功,就是没数据。

第二个坑是压缩。Caddy的encode gzip zstd对普通页面是神器,对SSE是毒药。压缩流需要知道"结尾"才能解压,SSE没有结尾。你必须把Mercure路由摘出来:

@not_mercure { not path /.well-known/mercure* }
encode @not_mercure gzip zstd

这段的意思是:除了Mercure的SSE端点,其他全压缩。如果哪天Mercure拆了,模板还能优雅降级成简单的encode gzip zstd。

第三步:JWT密钥的Ansible Vault方案

第三步:JWT密钥的Ansible Vault方案

打开网易新闻 查看精彩图片

发布端(你的PHP后端)和订阅端(用户浏览器)用同一套JWT验身份。密钥存在哪儿?硬编码是自杀,环境变量裸奔是半死。

作者的做法:defaults/main.yml里引用vault变量,实际密钥锁在Ansible Vault。Mercure启动时只读一个简单的环境文件:MERCURE_JWT_SECRET=xxx。

这个设计让轮换密钥变得可操作——改Vault,重跑Playbook,滚动重启,不用ssh进去手动改配置。

那些文档没写的调试信号

那些文档没写的调试信号

healthz端点200响应,是K8s时代留下的肌肉记忆。作者给Mercure配了respond /healthz 200,负载均衡器探活时用得上。

cors_origins和publish_origins的星号与域名搭配也有讲究。匿名订阅(anonymous)开不开,取决于你的业务是否允许未登录用户收推送。这些没有标准答案,但配置项的排列组合决定了你是"能用"还是"好用"。

整套方案跑通后,你的VPS上会有两个Caddy进程:一个面对公网,一个躲在localhost后面专门推流。它们不打架,因为端口和职责被严格切分。

最后留个开放问题:如果你的实时推送量从每秒100条涨到10万条,这个架构的瓶颈会先出现在主Caddy的反向代理层,还是Mercure的SSE连接池?你打算怎么验证?