必须用多阶段构建分离builder和runtime阶段,否则vendor中dev依赖、tests/、.git及未优化autoload将导致镜像膨胀数百MB;builder阶段需执行composer install --no-dev --prefer-dist --optimize-autoloader --classmap-authoritative,runtime阶段只精确复制/vendor等运行必需文件。

PHP容器镜像里vendor占几百MB?不是包太多,是build阶段没做裁剪——dev依赖、tests/、.git、未启用的autoload扫描全在悄悄膨胀镜像。
多阶段构建必须拆开builder和runtime两个阶段
builder阶段用含完整工具链的镜像(如php:8.3-cli-alpine),只干一件事:装依赖;runtime阶段换轻量镜像(如php:8.3-fpm-alpine),只收运行必需的文件。
- builder阶段必须复制
composer.json和composer.lock到最前,保证Docker层缓存命中 - 执行
composer install --no-dev --prefer-dist --optimize-autoloader --classmap-authoritative,不是update - runtime阶段禁止
COPY --from=builder /app .,只精确COPY --from=builder /app/vendor /var/www/html/vendor - builder和runtime的PHP minor版本必须严格一致(如都是
8.3.12),否则PECL扩展(如redis.so)会报undefined symbol
archive.excludes配置必须配合插件启用
这个配置只对packagist dist包生效,且默认不启用。不装插件、不写路径格式、不更新lock,它就等于没写。
- 先运行
composer require --dev composer-archive-excludes - 在
composer.json的config.archive.excludes下写路径,例如:"/tests/"、"/docs/"、"/.github/"——注意开头结尾都要有/ - 改完必须跑
composer update --lock再composer install,否则旧composer.lock会跳过该逻辑 - 对git clone的包、path repo、老包无效,得靠后续
find命令补刀
部署脚本里必须补上系统级清理命令
archive.excludes管不到的场景很多:本地repo、CI拉取的source、没维护.gitattributes的老包。这时候就得用find硬删,但有底线。
立即学习“PHP免费学习笔记(深入)”;
- builder阶段末尾加:
find vendor/ -name ".git" -type d -exec rm -rf {} + 2>/dev/null - 可追加删
CHANGELOG.md、README.md:find vendor/ \( -name "README.md" -o -name "CHANGELOG.md" \) -delete 2>/dev/null - 绝对不能删
vendor/autoload.php或vendor/composer/autoload_*.php,否则自动加载直接失效 - 别用
find vendor/ -name "tests" -type d -exec rm -rf {} +——有些包(如旧版monolog/monolog)把Handler示例放tests/下,删了就Class not found
运行时autoload必须强制走classmap
哪怕你删光了tests/和.git,只要没启用--classmap-authoritative,PHP autoload仍会尝试scandir()整个vendor/目录——在只读文件系统、NFS或容器里极易失败。
- 确保
composer.json中已设"optimize-autoloader": true - builder阶段执行
composer dump-autoload --optimize --classmap-authoritative --no-dev - 验证是否生效:检查
vendor/composer/autoload_classmap.php是否生成,且内容不为空 - 这个开关会让autoload完全忽略PSR-4映射路径下的文件扫描,只认classmap里登记的类——这才是真正“瘦身”后的运行态
最容易被忽略的是:--classmap-authoritative不生效时,删掉的文件其实没真正退出运行视野;而archive.excludes配错格式或没跑update --lock,等于白配。这两处一漏,前面所有操作都打折扣。



















