Composer本身不支持私有Packagist双活,所谓“双活”必须由基础设施层统一入口承载,而非靠repositories数组配置实现;它只按顺序请求首个可用源,遇超时或502直接报错,不探测、不并发、不自动降级。

Composer本身不支持私有Packagist双活
所谓“私有Packagist双活”,不是靠composer.json里多写几个repositories就能实现的。Composer 从不并发请求、不健康探测、不自动降级——它只按数组顺序找第一个能返回有效packages.json且含目标包的源,其余全被跳过。你配置了两个私有镜像地址,只要第一个卡在TLS握手或返回502,整个composer install就卡满http.timeout(默认30秒)后报错退出,不会试第二个。
常见错误现象包括:
-
composer update卡在Downloading https://private-mirror-a.example.com/p/provider-laravel~10.0.json,但curl -I https://private-mirror-b.example.com/packages.json明明通着 - CI构建因单点私有源DNS解析失败而中断,日志里看不到任何fallback尝试痕迹
- 两个镜像同步延迟不一致(比如A源刚发布v1.2.0,B源还停留在v1.1.9),导致同一
composer update在不同机器上解析出不同版本
真双活必须由基础设施层统一入口承载
要让私有Packagist看起来是“双活”,唯一可靠路径是把多个物理镜像节点抽象成一个逻辑域名,由外部组件完成流量分发与故障隔离。Composer只认这个单一URL,完全无感。
可行方案有三种,按落地成本排序:
- 用
Nginx或Caddy做反向代理集群:上游配置private-mirror-a.example.com和private-mirror-b.example.com,启用health_check和least_conn;关键路径如/packages.json、/p/加主动探活;响应头必须带Cache-Control: public, max-age=3600 - 用
Anycast IP托管统一域名(如repo.internal),背后挂华东、华南两套Satis静态文件服务,依赖BGP路由自动调度;单点宕机时,客户端自然切到另一节点 - 轻量级
composer-proxy(如yunwuxin/composer-plugin-mirror)跑在本地127.0.0.1:8080,内部轮询多个私有源,失败自动跳过;再通过COMPOSER_REPO_PACKAGIST=http://127.0.0.1:8080注入
注意:composer config -g repo.packagist只接受单URL,填两个会被静默忽略;repositories数组里的私有源不参与全局镜像替换,仅用于元数据合并。
Satis静态镜像双节点一致性是最大陷阱
Satis本身不是服务进程,只是个一次性构建工具。你跑两次php bin/satis build分别输出到两台Nginx,若没共享存储,packages.json里写的dist.url就会指向各自本地路径,下游composer install必然卡在Downloading (connecting)——因为ZIP文件根本不在那个URL下。
必须做到:
- 所有Web节点的
root指向同一份文件系统(NFS/MinIO/S3),不能靠rsync同步——有窗口期不一致 - CI发布必须原子操作:先在临时目录
build-$(date +%s)完整生成,再mv替换旧目录 - 每个节点都能
GET /packages.json返回200且内容SHA256一致;返回HTML欢迎页或404说明Nginx配置错位 -
composer.json里务必设"packagist.org": false,否则官方源仍会偷偷介入,覆盖私有包
客户端配置稍错,双活就形同虚设
哪怕后端做了完美双活,客户端一配错,流量照样打到单点。最容易被忽略的三个细节:
-
repositories数组中私有源必须放在第一位,且url末尾斜杠必须和satis.json中homepage完全一致:"https://repo.internal/"≠"https://repo.internal" - 禁用官方源不能只靠
"packagist.org": false,还要确认没在其他字段(如config)里意外开启secure-http: false导致降级到HTTP - CI环境必须显式设置
COMPOSER_MIRROR_URL=https://repo.internal,不能依赖本地~/.composer/config.json——容器每次启动都是干净环境
最常被低估的一点:双活≠高可用。如果两个Satis节点都同步自同一个上游Git仓库,而该仓库挂了,双活只是把故障复制了两份。


















