GitLab CI中composer install卡在Downloading packages.json,是因为镜像未配对或runner用户未读取到配置;根本原因是CI以gitlab-runner用户运行,其$HOME为/home/gitlab-runner,而本地配置写入的是/root/.composer/config.json,故必须在.gitlab-ci.yml中由runner当场执行严格校验的composer config -g命令。

GitLab CI里composer install为什么总卡在Downloading packages.json
因为镜像没配对,或者配了但没被runner用户读到。典型现象是日志里反复出现Downloading https://repo.packagist.org/packages.json,哪怕你本地执行过composer config -g也没用。
根本原因:CI job 默认以gitlab-runner用户运行,而composer config -g若在本地或用sudo执行,会把配置写进/root/.composer/config.json,但$HOME其实是/home/gitlab-runner,它完全不看那个文件。
- 先确认当前用户:
whoami或看日志开头的Running with gitlab-runner on ... - 检查配置路径:
echo $HOME+ls -la $HOME/.composer/config.json - 必须在
.gitlab-ci.yml中由runner当场执行,且参数严格校验:composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/composer config -g --unset github-protocols -
-g不能省,否则只改当前目录下的composer.json,不影响后续install - URL末尾必须带
/,否则拼出https://mirrors.aliyun.com/composer/packages.json→ 404 → fallback 到官方源
缓存~/.composer/cache但vendor/不缓存的真正原因
直接缓存vendor/看似省事,实则埋雷:它含平台相关二进制(如vendor/bin/phpunit符号链接)、扩展编译产物、autoload生成逻辑,跨runner环境极易不一致甚至autoload冲突。
真正该复用的是Composer下载解压前的归档包和元数据——全存在~/.composer/cache下,与PHP版本、OS、架构解耦。
- 必须显式设置:
export COMPOSER_CACHE_DIR="$HOME/.composer/cache"(推荐放在before_script) - 提前创建目录:
mkdir -p "$COMPOSER_CACHE_DIR",否则缓存写入失败 - 缓存
key必须基于composer.lock内容哈希:cache: key: files: [composer.lock],不能只用分支名或时间戳 - 如果项目需多PHP版本测试,加
prefix: "php-${PHP_VERSION}" -
composer.lock必须已提交且未被.gitignore排除,否则GitLab计算哈希时跳过该文件
composer install必须带的四个参数缺一不可
漏掉任一个,轻则安装变慢2–3倍,重则线上类加载失败或Fatal error。这不是“建议”,是CI构建的底线。
-
--no-interaction:禁用交互(包括GitHub token提示、post-install confirm),否则CI作业卡死 -
--prefer-dist:强制走zip包而非git clone,减少网络请求和解压开销;不加会回退到--prefer-source,极慢且易因SSH/Token问题失败 -
--no-dev:跳过require-dev,避免把phpunit、symfony/debug-bundle打进生产镜像,也防运行时Class 'PHPUnit\Framework\TestCase' not found -
--optimize-autoloader(或-o):生成vendor/composer/autoload_classmap.php,把PSR-4映射编译成静态数组,类加载提速50%以上
额外强推:--classmap-authoritative——配合前一个参数,彻底禁用file_exists()探测,既是性能关键,也是防止Class not found的最后一道防线。
为什么绝不能在CI里用composer update
CI不是开发机,它不参与“选版本”的决策。composer update会重算整个依赖树、生成新composer.lock,等于绕过代码审查偷偷升级包。昨天通过的流水线,今天可能因monolog/monolog一个patch引入BC break。
composer install才严格按已提交的composer.lock还原:版本、顺序、嵌套关系全部锁定。前提是这个文件必须存在且已提交——CI不会帮你生成,缺它命令直接报错退出。
- 常见错误:
Root package 'xxx' cannot be found,大概率就是漏提composer.lock - monorepo项目中,每个子包目录下都得有自己独立的
composer.lock,否则install自动退化为update - GitLab / GitHub Actions 默认拉取 clean checkout,只靠
composer.lock启动安装,不依赖本地状态
依赖更新必须单独走人工触发流程(比如每周定时job),不能混进日常构建。


















