先说一个可能有点反直觉的结论:一个项目能不能顺利走下去,往往不是看业务代码写得有多好,而是看第一天把项目环境和持续集成环境搭得有多扎实。这段视频标题是《Day01-06.搭建项目环境-持续集成环境》,时长只有14分27秒,表面看只是“装个工具”,但它背后真正要做的事,是把“代码提交之后的一切”自动化跑起来。这篇文章就按我实际操作环境搭建的过程来聊,把从环境规划、工具选型到流水线跑通、踩坑修复的完整链路讲清楚,适合正在从零规划项目环境的同学直接参考。
我见过不少团队,第一天拉仓库、写几行代码挺开心,结果第二周就开始为环境问题吵架。很多人以为持续集成环境是“大厂才需要的东西”,真不是。只要你的项目超过一个人维护,只要你还出现过“我本地是好的啊”这句话,持续集成环境就缺不了。
1. 为什么我把“搭环境”排在整个项目的第一位
先描述一下没有持续集成环境的时候,一个普通的周五下午会是什么样子。后端同学说接口改好了,前端同学说那我重新部署一下后端,结果登录服务器手动拉代码、手动编译、手动重启,折腾了二十多分钟,发现编译失败,因为后端本地能通过,但服务器上缺一个依赖。两个人开始互相甩锅,最后发现服务器上的代码已经不是最新版本了。
这种场景我遇到过太多次。再列几个没有 CI 时一定会出现的典型乱象:
- 测试拿到的包是上周二构建的,开发本地跑的却是最新代码,Bug 反馈过来根本没法定位,问题永远在“怎么复现”上打转。
- 新同事入职第一天,光配置本地开发环境就花了大半天,项目没跑起来,人先跑了一半。
- 上线前才发现生产构建依赖的环境变量没配,代码在本地好好的,打包到线上就挂。
- 谁改了代码、改了什么、构建过没有,全靠在群里喊一嗓子,没有任何可供追溯的记录。
1.1 这些问题的根子到底在哪
我后来总结,这些问题的根子不是某个人的操作失误,而是项目环境缺少一个自动化的衔接层。大家各干各的,没人对“集成结果”负责,所有问题都堆到上线前一天爆发。
很多团队觉得买台服务器、装个 MySQL、把项目跑起来就叫“搭建项目环境”了。这其实只是最基础的运行环境。对于一个正经开发的项目,环境至少应该包含三块:本地开发环境、持续集成环境、部署运行环境。本地开发环境解决“我一个人能不能写代码”的问题;持续集成环境解决“代码合在一起能不能跑”的问题;部署运行环境解决“用户能不能用”的问题。后面两块往往被严重低估,直到出事了才被人想起来。
1.2 持续集成环境到底在解决什么问题
持续集成环境要解决的最核心问题,其实只有两个字:反馈。它做的事情表面上很简单——代码每次提交到仓库,自动把最新代码拉下来,执行构建、跑测试、产出报告,然后把结果反馈给提交的人。可就是这个“简单”的动作,把集成错误从“上线前才发现”提前到了“提交后几分钟内”。
如果展开讲,一个完整的持续集成环境通常由四块组成:代码仓库(GitLab、GitHub 等)、CI 调度器(负责监听提交并分配构建任务)、构建执行器(真正跑编译和测试的地方)、产物存储。你把它想象成一条流水线:原料是代码,加工是编译测试,质检是测试报告,最终包装是构建产物。任何一个环节断裂,整条线就得停。
我当时搭这套环境,遵循的就是一条原则:宁可第一天多花四个小时把环境弄顺,也不要后面每周都花四小时去填手动部署的坑。环境建设这种事,越早做,收益就越大;越拖到后面,返工成本就越高。
2. 动手前先想清楚:环境规划与工具选型
网上教程很多,大多数一上来就让你装 Jenkins。但我自己的经验是,先别急着装工具,把环境分层和工具选型想清楚,能省掉后面很多返工。这是整个搭建过程中最容易被忽略、却是最影响结果的一步。
2.1 开发、测试、生产到底怎么分层
很多人觉得“环境”就是一台服务器,其实它不是。项目环境至少要分三层,每层的使命完全不同:
| 环境 | 用途 | 稳定性要求 | 谁在用 |
|---|---|---|---|
| 开发环境 | 本地写代码、调试 | 不太稳定没关系 | 每个开发者 |
| 测试/集成环境 | 联调、验证版本 | 需要稳定且始终可用 | 测试、前端、后端 |
| 生产环境 | 对外提供服务 | 最高 | 最终用户 |
持续集成环境属于“测试/集成环境”的上游,它的职责就是在代码进入测试环境之前,先把“能不能编译、能不能通过核心测试”这件事自动化。换句话说,它是一道质量门禁:代码过了这道门,才有资格被部署到测试环境。
我见过不少团队,这个分层没有真正建立起来,大家共用一台服务器,开发改的代码和测试验证的版本混在一起。结果就是:这周测试说连不上数据库,开发跑过去一看,数据库里多了一张开发调试用的临时表,全部删掉重来。环境不稳定导致的返工和误判,比一个特定的 CI 工具选错了还要伤。
2.2 三种主流 CI 工具怎么选
明确分层之后,再来看工具。现在最主流的持续集成工具有三拨:GitLab CI、Jenkins、GitHub Actions。我先把对比表格给出来,再讲讲怎么选:
| 维度 | GitLab CI | Jenkins | GitHub Actions |
|---|---|---|---|
| 代码仓库集成 | 原生集成 | 靠插件 | 原生集成 |
| 配置方式 | .gitlab-ci.yml | 界面+Jenkinsfile | .github/workflows |
| 学习成本 | 低 | 中高 | 低 |
| 资源占用 | 轻量 | 较重 | 托管运行 |
| 适合场景 | 代码在 GitLab | 复杂存量项目 | 代码在 GitHub |
我当时项目代码放在自建的 GitLab 上,所以直接选了 GitLab CI。理由很实在:它不用单独装一套 Web 服务,仓库和 CI 的权限天然打通,开发者提交代码就能在仓库页面直接看到流水线状态,不用再去另一个系统里查。如果你代码在 GitHub,选 GitHub Actions 也一样顺手;如果公司有大量存量 Jenkins 插件资产,那就不要盲目迁移,把 Jenkins 配置规范好就行。
我个人的建议是:在没有专职 DevOps 角色的团队里,优先选和代码仓库同生态的 CI 工具,少一套系统就少一份维护成本。
2.3 为什么我让 Docker 当整个环境的底座
这一步可能很多教程不讲,但它是我搭建过程中最正确的决定:所有组件都用 Docker 方式部署。代码仓库部署成容器、CI 执行器跑在容器里、构建环境也用官方镜像。
选 Docker 的理由很朴素。第一,环境一致性:你用maven:3.8-openjdk-11镜像编译,那 CI 里的 Java 版本就永远是 11,不会出现本机 JDK 17、服务器 JDK 8 这种神奇问题。第二,隔离和清理:一个构建任务结束,容器销毁,环境回到干净状态,不会残留上次构建产生的临时文件。第三,快速复制:要加一台构建机器,装好 Docker 拉个镜像就行,不用逐步骤配置环境。
当然 Docker 也不是银弹,网络、存储、权限都会带来新坑,这部分后面专门用一整章讲。但作为整个持续集成环境的底座,它让所有上层组件的安装和升级变成“停容器、拉新镜像、再启动”三步,这个收益在后续维护阶段会持续放大。
3. 服务器到跑通流水线:我是怎么一步步把环境立起来的
规划做完,下面进入实操。我以一台 Ubuntu 22.04 服务器为例,走一遍从服务器初始化到第一条流水线跑通的完整过程。你手里如果是其他发行版、虚拟机,甚至云主机,命令上要相应调整,但整体思路是一致的。
3.1 第一步:服务器基础准备
拿到一台新服务器,第一件事不是装 Docker,而是先建一个非 root 的普通用户。很多教程直接让你用 root 干所有事,短期看着爽,后面所有服务都跑在 root 下,权限一乱就非常难收场。我用的是deploy用户:
sudo apt update && sudo apt upgrade -y sudo useradd -m -s /bin/bash deploy sudo usermod -aG sudo deploy然后装 Docker。这里直接用官方脚本快速安装,装完记得把当前用户加进 docker 组,不然每次执行 docker 命令都得加 sudo:
curl -fsSL https://get.docker.com | sh sudo usermod -aG docker deploy装好后验证一下:docker version能看到客户端和服务端版本就算成功。注意,加完用户组要重新登录,或者执行newgrp docker让权限生效,否则会报permission denied。这个细节新手最容易卡住。
如果拉镜像比较慢,可以给 Docker daemon 配置镜像加速地址,具体就是编辑/etc/docker/daemon.json,加上registry-mirrors配置,然后重启 Docker。这一步不强制,但建议做,后面拉 GitLab、Runner 镜像能快不少。
3.2 第二步:用 Docker 部署 GitLab 代码仓库
因为要用 GitLab CI,所以先得有 GitLab。部署方式很简单,官方镜像一行docker run,但参数里有几个细节值得说清楚:
sudo docker run --detach \ --hostname gitlab.local \ --publish 80:80 \ --publish 443:443 \ --publish 2222:22 \ --name gitlab \ --restart always \ --volume /srv/gitlab/config:/etc/gitlab \ --volume /srv/gitlab/logs:/var/log/gitlab \ --volume /srv/gitlab/data:/var/opt/gitlab \ gitlab/gitlab-ce:latest几个容易踩的坑先给你标出来。--hostname指定的是你后续访问 GitLab 的域名,如果暂时没有域名,建议直接填服务器的 IP,千万别乱填一个以后根本不用的域名,后面改起来会比较麻烦。--publish 2222:22是因为宿主机可能已经占用了 22 端口,把 GitLab 的 SSH 端口映射到 2222,这样 clone 代码仓库时用的是ssh://git@服务器IP:2222/...。
GitLab 第一次启动要做初始化,这个过程根据机器配置可能要三到五分钟。用docker logs -f gitlab盯日志,出现GitLab is ready之类的字样就说明起来了。这中间服务会间歇性返回 502,属于正常现象,别急着重启容器。
3.3 第三步:安装并注册 GitLab Runner
代码仓库有了,接下来是真正干活的 CI 执行器。Runner 是整个 CI 里最容易让人懵的一个角色:它本身是一个常驻服务,负责从 GitLab 领取任务、启动 Docker 容器去跑你定义的构建脚本。
先跑一个 Runner 容器:
sudo docker run --detach \ --name gitlab-runner \ --restart always \ --volume /srv/gitlab-runner/config:/etc/gitlab-runner \ --volume /var/run/docker.sock:/var/run/docker.sock \ gitlab/gitlab-runner:latest这里挂载 docker.sock 是让 Runner 能够“指挥”宿主机的 Docker 去创建构建容器,这也是最常用的 Docker executor 模式。不过要提醒一句,把 docker.sock 暴露给 Runner,等于给了 Runner 创建所有容器的权限,团队规模小的时候问题不大,生产环境需要更谨慎的隔离方案。
然后执行注册命令:
sudo docker exec -it gitlab-runner gitlab-runner register按照提示依次输入:GitLab 地址(http://服务器IP)、注册 token(在 GitLab 项目或群组的 Settings > CI/CD > Runners 里找)、Runner 描述、标签(比如填java)、执行器类型(填docker)、默认镜像(填maven:3.8-openjdk-11或alpine:latest)。
注册完成后的 Runner 会出现在 GitLab 的 Runner 列表里,状态显示绿色就说明上线了。注册 token 的本质是 Runner 和 GitLab 之间的信任凭据,所以千万别把 token 泄露到公开仓库里,它是你 CI 环境的通行证。
3.4 第四步:提交第一条流水线
Runner 就绪后,最关键的一步来了:在项目根目录创建.gitlab-ci.yml。这个文件就是流水线的“剧本”。我用一个 Java Spring Boot 项目举例,完整内容如下:
stages: - build - test build-job: stage: build tags: - java script: - mvn clean package -DskipTests artifacts: paths: - target/*.jar expire_in: 7 days test-job: stage: test tags: - java script: - mvn test needs: - build-job解释三个关键点。stages定义了阶段顺序,每个 job 必须指定属于哪个 stage,只有前一个 stage 里所有 job 都成功后,后一个 stage 才会启动。tags很关键,它用来匹配 Runner 注册时填的标签,Runner 没有对应 tag,job 会一直停在 pending。artifacts是产物收集,构建好的 jar 会被 GitLab 归档保存,后续任务可以直接下载使用。
把文件推到仓库后,在 GitLab 页面的 CI/CD > Pipelines 里就能看到流水线开始跑。第一次跑通的那一刻,你的持续集成环境就算立起来了:以后每次 push,系统都会自动执行你定义的全部检查。
4. 让流水线真正好用的五个配置细节
流水线能跑和“好用”,是两码事。我自己跑通第一条流水线的时候挺兴奋,但用了一周才发现,很多细节不处理,这个 CI 反而会变成新的负担。这一章讲五个我反复踩过、后来又重点优化的细节。
4.1 构建脚本必须“可重复执行”
CI 里的构建脚本和本地敲的命令有本质区别:它要共享、可追溯、会被反复执行。所以第一条原则是:不要依赖本地 IDE 才有的功能。比如有些人习惯让 IDEA 帮忙编译,或者用了 Lombok 但没在 pom 里声明,本地一切正常,CI 里直接编译失败。写脚本的时候,要用标准的构建命令,并且保证脚本在空环境里跑也成立。
另外,脚本要能反过来指导本地开发。我的做法是:把构建脚本尽量和本地保持一致,本地能跑,CI 也应该能跑;只要有任何差异,就记录原因,这样后面环境变更有据可查。
4.2 依赖缓存:控制构建时长和稳定性
流水线平均跑多久,决定了你愿意多大频率提交代码。如果一次构建要五分钟,很多人会“攒一堆再提”;如果只要三十秒,大家会频繁提交,Bug 也能更早暴露。依赖缓存就是这个博弈里的关键变量。
GitLab CI 提供了cache关键字,比如 Maven 项目可以这样配置:
cache: key: maven-cache-$CI_COMMIT_REF_SLUG paths: - .m2/repository/这样同一个分支的依赖只用下载一次,后续构建直接复用缓存,构建时间能缩短一半以上。注意cache和artifacts的区别:cache 是给后续 job 复用依赖的“中间缓存”,可以随时失效;artifacts 是必须保存并传递的“最终产物”,用来产出报告和安装包。两者混用是新手常犯的错误,会导致缓存越来越大、上传下载越来越慢。
4.3 敏感信息永远不进仓库
CI 脚本里经常需要数据库连接串、部署服务器的密钥,这些千万不能写死在 yml 里再提交仓库。GitLab 的 CI/CD Variables 功能就是干这个的:在项目 Settings > CI/CD > Variables 里添加键值对,然后在脚本里用$变量名引用。
这里有一个变量分层的实用技巧:相同变量在群组和项目里各设一份,项目里的变量优先级更高,可以实现“群组统一配置、项目按需覆盖”。密码这类变量建议勾选 Masked,这样在日志里不会明文打出来。我见过太多团队这一条没做,数据库密码躺在 GitLab 历史记录里,删都删不掉,这种安全隐患比任何 Bug 都严重。
4.4 失败的反馈要直达“犯错的人”
CI 跑失败不可怕,可怕的是失败之后没有人知道。GitLab CI 内置的通知邮件可以把每次流水线结果发给提交者,但邮件太容易被忽略。更有效的做法是接 webhook:很多团队喜欢接到内部协作工具,提交后几分钟,群里自动弹出“某分支流水线失败,请查看”。
配置方式很简单,在协作机器人的 webhook 地址填上去,然后选择对应事件即可。我建议至少开启 pipeline 失败和成功两个事件,失败通知带上提交人、流水线链接、失败阶段,看到通知的人可以立刻点进去定位,不用在群里追问“谁提交的”。
4.5 给不同的分支定义不同的策略
不是所有分支都应该跑完全部流程。我在项目里的基础策略是:主分支跑构建+测试+部署测试环境的全部流程,开发分支只跑构建和测试,功能分支甚至可以只做静态检查。区分的方式是在 job 里使用rules条件:
deploy-test: stage: deploy script: - ./deploy-test.sh rules: - if: '$CI_COMMIT_BRANCH == "main"'这样做的好处很明显:开发分支的反馈周期被压缩到最短,主分支则承担最重的质量门禁。没有 rules 的时候,所有分支一视同仁,一旦有人从开发分支反复推代码,流水线队列就会堵成一团,真正关键的构建反而要排队。
5. 实测踩坑记录:环境搭好了,怎么还跑不起来
就算你照着上面的步骤完整操作,大概率还是会遇到问题。我把搭建和后续一个月真实使用里遇到的三个典型问题整理出来,重点讲排查思路,因为思路比答案更值钱。
5.1 权限与文件归属:Runner 缓存目录的“幽灵”权限问题
有一天我发现某个 job 构建失败,日志显示缓存目录Permission denied。一开始我以为是磁盘满了,执行df -h一看,还有一半空间。后来仔细看日志,才发现是权限问题。
根因是这样的:Runner 默认以容器内用户执行构建,构建时产生的缓存文件,在宿主机上的属主可能变成一个特定的 UID(比如 1000),但下一次运行时容器内用户的 UID 变了,没有权限读写。解决办法有两个层面:第一,在 Runner 配置里给构建指定用户,保持 UID 一致;第二,如果是 Docker executor,让构建容器在宿主机用户下运行,再配合调整缓存目录属主。
这个坑最麻烦的地方在于它“偶发”:跑一天没事,第二天突然失败,非常容易误判成资源或网络问题。我的排查经验是,遇到诡异的构建失败,先看完整日志里有没有Permission denied,有的话别急着重启 Runner,先检查缓存目录的属主。
5.2 依赖下载慢到超时:处理好镜像与网络问题
第二个高频坑是构建超时。一个小项目 Maven 构建平时一分多钟,有一天突然跑了二十分钟还没结束,最终被 GitLab 的默认超时时间卡住。一查,是 Maven 中央仓库当时网络不稳定,依赖下载速度极低。
解决方案是双管齐下。第一,给 Maven 配置国内镜像源,这个在settings.xml里加 mirror 配置即可;第二,在 GitLab Runner 配置里调小 job timeout,比如 20 或 30 分钟,让超时问题更早暴露,而不是白白等一小时。依赖下载慢还有一个很实际的处理方法:在 CI 配置里把需要复用的依赖目录放进 cache,这样第一次慢,后面都快。
5.3 并发不足:一台小机器被三个 job 打垮
第三步是执行能力问题。我一开始在 Runner 上没限制并发,结果前端、后端、测试三个 job 同时触发,小机器内存直接爆掉,Docker 守护进程被系统杀掉,所有构建一起失败,整个环境像被“打挂”了。
排查方法很直观:docker ps发现容器全部退出,free -h看到内存用完,dmesg里有 OOM 记录。解决办法是限制 Runner 的并发上限,在 Runner 的config.toml里设置concurrent = 1,或者给不同项目分配不同的 Runner tag,让重活分开跑。我后来还顺手加了 swap 空间作为兜底,但真正健康的做法是:先限制并发,再接监控,最后才考虑加机器。
6. 搭完之后怎么验收,以及它的下一步
环境搭完不等于可以撒手不管。我自己有个验收清单,每次搭完 CI 环境,都会按照这个列表过一遍,确认不是“表面跑通”。
6.1 一份简单但完整的验收清单
| 检查项 | 操作方法 | 通过标准 |
|---|---|---|
| 触发链路 | 修改代码推到仓库 | push 后自动产生新 pipeline |
| 构建正确性 | 故意引入一个编译错误 | 流水线尽快失败并显示对应阶段 |
| 产物归档 | 查看 build job 的 artifacts | 能下载到 jar 或前端包 |
| 通知反馈 | 看协作工具机器人 | 失败和成功都收到消息 |
| 权限隔离 | 查看变量的 Masked 属性 | 日志中不会出现明文密码 |
| 缓存复用 | 连续跑两次同一分支 | 第二次时长明显缩短 |
这套清单配合上面的五个配置细节,基本覆盖了环境的可用性。真正投入使用之前,我还会让团队里每个人都自己提交一次代码,而不是只有我自己跑通。因为 CI 是给人用的,如果负责前端、后端的同学在提交代码时发现流程不理解或脚本报错,说明环境还缺文档,或者自动化程度不够。
6.2 从持续集成走向持续部署
如果你已经把持续集成跑稳定了,下一步自然是持续部署:让通过测试的构建自动部署到测试环境。GitLab 本身就支持在 pipeline 里加 deploy 阶段,也可以用其他自动化工具来做。但我的建议是:先把 CI 这一层吃透,不要一上来就搞 CD 全家桶。
原因很简单:CD 对环境的稳定性、安全性和权限管理要求更高,如果 CI 还没稳定就叠加部署功能,一旦出了问题,你连“是部署的问题还是构建的问题”都区分不出来。我见过不少团队,CI 没稳就开始自动发布,结果测试环境被一条错误的流水线改坏,最后不得不回滚代码、反思权限管理。
我个人搭了不下十套 CI 环境,总结下来最值钱的不是某一种工具用得多熟练,而是第一天就建立起的“自动化意识”:每次提交都能快速得到反馈,每个改动都有记录,任何人都能从零复制一套环境。环境建设这种东西,短期看不到什么成果,但它决定了项目后期能走多顺。第一天多花四小时,后面省下的绝对不止四十小时。你要是也想搭一套持续集成环境,建议就从今天动手,先把 GitLab Runner 这一条链路跑通,后面再逐步完善也不晚。