![]()
一台服务器上跑两个Caddy,就像一间公寓里住了两个房东,都觉得自己该收房租。Mercure自带的嵌入式Caddy非要占443端口,可你主Caddy早就在那儿了。冲突不是"可能",是一定。
这是部署Mercure(一种基于服务器推送事件/SSE的实时协议)时最隐蔽的坑。很多人按官方文档装完,发现服务起不来,日志里全是端口占用。本文用Ansible完整复现一套共存方案,包括三个你Google不到的细节。
第一步:把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配置看起来标准,但有两个参数写错就全完。
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方案
![]()
发布端(你的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连接池?你打算怎么验证?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.