生产环境 docker 镜像中出现 phpunit 或 symfony/debug-bundle,说明 --no-dev 未生效或未添加;docker 构建必须用 composer install --no-dev 而非 update,因其严格按 composer.lock 精确还原依赖,不重解析、不改锁文件、不引入传递 dev 依赖,确保部署确定性与安全性。

生产环境 Docker 镜像里出现 phpunit 或 symfony/debug-bundle,基本可以断定 --no-dev 没生效,或者没加。
为什么 Docker 构建必须用 composer install --no-dev,而不是 update
部署阶段要的是确定性:按 composer.lock 精确还原,不改依赖树、不引入新版本、不触发重解析。composer update --no-dev 会重新走依赖求解器,哪怕跳过 require-dev,仍可能因传递依赖把某个 dev-only 工具(比如某包在它的 require-dev 里声明了 phpstan,又被其 require 间接拉进来)带进 vendor。install 则严格只装 lock 里记录的生产包——只要 lock 本身不含 dev 包,--no-dev 就能干净跳过。
常见误操作:
- CI 流水线里用
composer update --no-dev更新 lock 后直接进构建,结果packages-dev字段被清空,但某些包的 dev-only 依赖已悄悄混入packages; - 本地
composer update没加--no-dev,导致composer.lock记录了phpunit,后续install --no-dev也救不回来——得先删 lock、重install或手动清理。
--no-dev 在 Dockerfile 里怎么写才真正起作用
参数写了不等于生效。Docker 构建中常见的“假生效”有三类:
-
RUN composer install --no-dev前没COPY composer.json composer.lock ./,而是COPY . .后再跑 install —— 这会让任何代码变更都强制重装整个 vendor,缓存失效,还容易漏掉--no-dev; - 用了
composer dump-autoload -o替代install --no-dev—— 前者不删 vendor 里的 dev 包,也不清理autoload-dev规则,Tests类照样能被加载; - CI 中复用未区分场景的
vendor/缓存 —— 上次构建含 dev 包,这次即使加了--no-dev,Docker 仍从缓存取旧 vendor。
正确写法(多阶段推荐):
# syntax=docker/dockerfile:1 FROM php:8.3-cli AS builder WORKDIR /app COPY composer.json composer.lock ./ RUN composer install --no-dev --optimize-autoloader --classmap-authoritative --no-interaction <p>FROM php:8.3-cli-slim WORKDIR /app COPY --from=builder /app/vendor ./vendor COPY . . </p>
验证 --no-dev 是否真生效,别信日志信文件
构建完镜像后,别看控制台有没有报错,直接进容器查三处:
-
ls vendor/—— 确认没有phpunit/、mockery/、phpstan/等目录; -
grep -r "Tests\" vendor/composer/autoload_static.php—— 应无匹配; -
docker run -it your-image php -r "require 'vendor/autoload.php'; var_dump(class_exists('TestsTestCase'));"—— 必须返回bool(false)。
最常被忽略的一点:--no-dev 只控制“装不装”,不负责“删不删”。即使参数生效,vendor/bin/phpunit、vendor/*/tests/、甚至 vendor/composer/installed.json 里残留的 dev 包元信息,都可能还在。需要靠多阶段构建的 COPY --from=builder 或显式 RUN rm -rf vendor/bin/*tests* vendor/*/tests 清理。











