GitLab CI/CD 确实能自动构建 Python 环境,但能不能顺利跑起来,关键看 .gitlab-ci.yml 里有没有把依赖隔离、缓存复用、虚拟环境路径陷阱这些事安排明白。很多新手在本地跑得欢,一到 CI 就翻车,问题大多出在这几个地方。
为什么 pip install -r requirements.txt 在 CI 里总翻车?
本地能跑不代表 CI 就能跑——CI 跑在干净的 Docker 镜像里,既没有全局 venv,也没有本机习惯的 ~/.cache/pip。常见的报错,比如 ModuleNotFoundError: No module named 'setuptools' 或者 Permission denied,说到底,要么是没搞用户级安装,要么是压根没激活虚拟环境。
- 创建隔离环境,铁打不动的规矩是
python -m venv .venv,别图省事用virtualenv,很多基础镜像里根本没预装它。 - 激活环境必须用
source .venv/bin/activate,而且后续所有pip操作都得在这个上下文里执行,否则相当于白忙活。 - 加
--user参数?那是临时的权宜之计,会污染系统路径,在 CI 这种追求干净重现的环境里,是大忌。正确的做法是确保pip命令本身就在.venv里。 - 镜像选择上,
python:3.11-slim比全量版python:3.11启动更快,但如果你的依赖包需要编译 C 扩展,就得手动把gcc装进去。
怎么让 pip 缓存不至于每次都“从零开始”?
GitLab CI 的默认行为是每次作业都开一个全新的容器,~/.cache/pip 这个目录根本不会保留。如果不配置缓存,一个中等规模的项目,光装依赖就得花掉 2 到 5 分钟,这谁受得了。
- 解决方法很简单,在
.gitlab-ci.yml的顶层加上cache:配置,把缓存目录指向pip的缓存文件夹:
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- ~/.cache/pip
- 这里有个坑:路径必须写成
~/.cache/pip,不能偷懒写成相对路径.cache/pip。因为 GitLab Runner 的家目录是动态挂载的,相对路径会找不到北。 - 另外,千万不要把
.venv目录也加到cache.paths里。虚拟环境里的二进制文件既不跨平台,也不兼容不同的 Python 版本,缓存了反而会引发ImportError。 - 一个小建议:在安装依赖前,先跑一句
pip install --upgrade pip,避免因为 pip 版本太旧,解析pyproject.toml时出幺蛾子。
before_script 和 script 到底该怎么分工?
很多人把 before_script 当成一个“命令垃圾桶”,什么玩意儿都往里塞。这会导致命令执行顺序错乱,甚至权限报错。分工必须明确:
before_script只负责“环境准备”:创建 venv、激活环境、升级 pip、设置环境变量(比如PIP_DISABLE_PIP_VERSION_CHECK=1来关掉版本检查的啰嗦提示)。script才负责“干正事”:安装依赖、运行测试、打包构建。- 有个关键点必须记住:激活命令
source .venv/bin/activate必须在每个script步骤的开头重新执行一遍。因为 GitLab CI 的每个script都是一个独立的 shell 进程,上一个步骤的激活状态不会自动继承下来。 - 如果项目用的是
poetry,别在before_script里直接poetry install。它默认会把包装到全局的~/.cache/pypoetry里,正确的做法是poetry install --no-root,并配合cache来保存~/.cache/pypoetry目录。
测试都过了,为什么部署时还是 import 失败?
这是一个最容易忽视的陷阱。流水线里 pytest 跑得欢,不代表你的包已经正儿八经地安装成一个可导入的模块了。很多项目图省事,直接 python test/test_main.py,完全绕过了包的安装流程。结果本地开发没问题,但 CI 构建出来的 wheel 文件里,要么缺了 __init__.py,要么 setup.py 的配置有硬伤。
- 解决办法是在
script里显式地执行pip install -e .(开发模式)或pip install .(生产模式),然后再跑测试。这样才能真正验证import路径是不是通的。 - 记得回头检查一下
setup.py或pyproject.toml里的packages配置,确认它包含了所有子模块。find_packages()默认不会递归查找空目录,容易漏掉。 - 如果你的项目用的是
src/目录结构,setuptools里必须配置package_dir={"" : "src"},否则就算pip install -e .成功了,代码里也找不到你的模块。

说到底,真正卡住人的从来不是什么高深的语法,而是 venv 激活的 shell 生命周期、缓存路径的绝对性,以及“本地能跑”和“CI 可重现”之间那层薄薄的信任差。把这些细节抠明白,流水线才能跑得又快又稳。