根本原因是docker层缓存被打断;应先copy composer.json和composer.lock再执行composer install,确保输入稳定、命中缓存,并在builder阶段完成安装后仅复制vendor到final阶段。

为什么 composer install 总是重装依赖,Docker 构建变慢?
根本原因不是 Composer 本身慢,而是 Docker 层缓存被意外打断——只要某一层的输入变了,它和之后所有层都会失效重建。最常见的情况是:把源码 COPY . /app 放在 composer install 前面,导致每次改一行 PHP 代码,整个 vendor/ 都得重装。
如何让 composer install 稳定命中缓存?
核心原则:让依赖安装这一步的输入尽可能稳定、且早于业务代码变更。关键操作如下:
- 先
COPY composer.json composer.lock ./(只复制这两个文件),再运行composer install --no-dev --prefer-dist --optimize-autoloader - 确保
composer.lock在 git 中已提交,且与composer.json版本匹配;否则install会退化为update,破坏确定性 - 用
--no-dev减少体积和安装时间,生产镜像不需要phpunit或symfony/debug这类包 - 加
--prefer-dist强制走压缩包而非 Git 克隆,更快更稳定;某些私有包若没配 dist URL,才需回退到--prefer-source
vendor/ 目录该不该 COPY 进镜像?
不该。直接在构建阶段生成 vendor/,而不是从宿主机 COPY 进来。原因很实际:
- 宿主机的
vendor/可能含平台相关扩展(如ext-mysqlnd编译产物),和容器内 PHP 版本或 ABI 不兼容 - Docker 构建是隔离环境,
composer install必须在目标镜像的 PHP 环境中执行,才能保证autoload_classmap.php路径、扩展加载逻辑正确 - 如果真想复用本地 vendor(比如开发时加速),应通过
VOLUME或绑定挂载,而非写进镜像层
多阶段构建里,composer install 放哪一阶段最安全?
放在 builder 阶段,并只 COPY 生成物(如 vendor/)到 final 阶段。但注意两个坑:
- 不要在 builder 阶段用
--no-interaction --quiet导致错误静默失败;至少保留--no-interaction+--verbose(或日志重定向),否则 CI 上出错难排查 - final 阶段的 PHP 版本必须和 builder 阶段一致,否则
opcache.preload或phar.readonly=Off类配置可能引发运行时异常 - 如果项目用了
composer config platform(例如锁死"php": "8.2.12"),builder 阶段的 PHP 版本必须严格匹配,否则install会忽略该约束或报错
最易被忽略的是 composer.lock 文件的更新节奏——它不该由 CI 自动 update,而应由人手动 composer update 后提交。否则每次构建都可能拉新版本,缓存形同虚设,线上行为也不可控。