根本原因是Docker层缓存失效:错误地将composer.json/lock放在COPY后期,导致代码变更时vendor层无法复用;正确做法是先COPY锁文件再install,最后COPY源码。

为什么 composer install 在 Docker 里每次都很慢
根本原因不是 Composer 本身慢,而是默认的 Docker 构建流程把 composer.json 和 composer.lock 放在了 COPY 后期,导致每次代码变更都会让前面所有依赖安装层失效。Docker 层缓存(layer cache)一旦断掉,composer install 就得重装全部 vendor/,没走任何缓存。
- 典型错误写法:先
COPY . /app,再RUN composer install—— 只要改一行 PHP 代码,整个 vendor 层就废了 - 正确思路是「分阶段 COPY」:先只拷贝锁文件和配置,跑安装;再拷贝源码
-
composer.lock必须存在且内容稳定,否则缓存命中率归零
怎么写 Dockerfile 才能让 vendor 被缓存住
关键在于让 composer install 这一层尽可能早地执行,并且只依赖不会频繁变动的文件。
-
COPY composer.json composer.lock ./—— 注意路径结尾的./,别漏掉 -
RUN composer install --no-dev --prefer-dist --optimize-autoloader—— 加这些参数不只是为了快,更是为了产出可复现、无副作用的 vendor -
COPY . .放在这之后,这样源码变更完全不影响前面的依赖层 - 如果项目用到私有包或自定义 repo,记得在
RUN前加COPY auth.json .并确保它不随代码频繁更新
缓存失效的几个隐蔽雷点
即使结构写对了,以下情况仍会让缓存“看起来生效”,实则每次都在重装:
-
composer.lock文件被 Git 的换行符自动转换(CRLF ↔ LF),导致哈希不一致 → 检查git config core.autocrlf,建议设为false或input - Docker 构建时用了
--no-cache或 CI 环境清了 builder cache -
composer install命令里混进了--ignore-platform-reqs或--update-with-dependencies—— 这些会绕过 lock 文件约束,直接触发重解析 - 基础镜像升级(比如从
php:8.2-cli换成php:8.3-cli),整个 RUN 层重新计算,缓存链断裂
要不要用 composer install --dry-run 验证缓存是否真生效
不用,它只模拟,不生成实际文件,无法反映层缓存行为。真正验证方式只有两个:
- 连续两次
docker build .,观察日志里composer install那步是否显示Using cache - 构建完成后进容器执行
ls -la vendor/ | head -5,对比两次输出是否一致(尤其注意autoload.php修改时间) - 如果用了多阶段构建,注意 final 镜像里
vendor/是从 builder 阶段COPY --from=builder /app/vendor /app/vendor过来的 —— 这个 COPY 本身不参与缓存,但上游 builder 阶段的缓存必须有效
最麻烦的其实是团队协作时 lock 文件没及时提交,或者有人本地改完没 run composer update 就直接 push —— 这种问题不会报错,但会让所有人失去缓存收益,而且很难定位。


















