做研发这几年,GitLab CI/CD 是我用下来最顺手的一套自动化工具链。它跟代码仓库长在一起,提交代码触发的时机、运行环境、产物收集、部署流程全部集中在一个配置文件里,团队里的同学在代码评审的时候顺便就能看到本次提交的自动化状态,这是 Jenkins 很难给你的体验。这篇文章我想从一次完整的实战讲起:一台服务器、一套 GitLab CE、一个前端项目,从 git push 开始,自动跑测试、构建、推送到服务器并完成部署,整条 Pipeline 走完大概三分钟。内容偏实操,适合已经搭过 GitLab、想解决“每次发布靠手点”这个问题的同学;如果你还没装过 GitLab,第二部分也给了完整的部署姿势,跟着敲一遍就能把环境立起来。
我会把这次实战拆成五块:整体设计思路、环境准备、Pipeline 文件编写、Runner 注册调度、常见问题排查。每块都会讲清楚为什么这么选、怎么落地、以及我踩过的坑。特别是最后一部分,我把网上流传的几个高频报错——比如 422 登录错误、login failed. check api token or gitlab version、restore 报无权限——都做了一遍原因分析,希望能帮你少走弯路。
1. Pipeline整体设计与思路拆解
1.1 为什么选GitLab CI/CD而不是Jenkins
先说一个很多团队纠结过的问题:已经有了 Jenkins,为什么还要上 GitLab CI/CD?我的判断标准很简单,看你的团队规模和发布频率。
Jenkins 本质上是一个通用自动化平台,它的优势是插件生态极其丰富,什么语言、什么场景都能覆盖。但代价也很明显:插件版本兼容、Master/Agent 节点维护、权限体系独立于代码仓库、Pipeline 脚本和仓库代码分离,这些都要额外花精力去维护。小团队往往只有一两个人兼职运维,光处理 Jenkins 本身的升级和插件冲突就够头疼的。
GitLab CI/CD 是 GitLab 内置的能力,配置文件 .gitlab-ci.yml 直接放在仓库根目录,跟代码走同一个变更流程。代码合并请求里就能看到 Pipeline 状态,MR 通过之后才允许合入,这种“质量门禁”是天然的开发流程管控。而且 Runner 架构足够轻,一个项目对应一个 Runner 都可以,资源不够就加机器,不需要像 Jenkins 那样维护一套复杂的调度体系。
我的建议是:如果你的团队在五十人以内、部署对象主要是实验室或中小型业务系统、发布频率一到两次每天,那 GitLab CI/CD 是性价比最高的选择。如果对自动化有非常特殊的定制需求,比如要接各种各样的外部系统、要做复杂的参数化构建矩阵,再考虑 Jenkins 也不迟。
1.2 从提交到部署:一次完整流水线要经过哪些站
很多人第一次接触 GitLab CI/CD 的时候容易被一堆概念绕晕,什么 stages、jobs、artifacts、runner、pipeline。其实你可以把 Pipeline 想象成一条生产线:代码提交是原材料进场,每道工序是一个 job,多个 job 按照依赖顺序组成 stages,所有 stages 串起来就是一条 Pipeline。
以我这次做的前端项目为例,完整链路是这样:开发者在本地写完代码 → git push 到 GitLab 仓库 → GitLab 检测到分支有新的 commit → 触发对应分支的 Pipeline → 第一阶段安装依赖 → 第二阶段跑单元测试和代码检查 → 第三阶段构建生产包 → 第四阶段把构建产物部署到目标服务器 → 部署完成后由脚本通知相关人员或直接展示在 MR 页面。整个过程不需要任何人登录服务器手动执行命令。
这里有个很容易混淆的概念:Pipeline 和 Job 的关系。一个 Pipeline 由多个 Job 组成,Job 是执行单元,它会在 Runner 上跑;而 Runner 是真正干活的机器,它向 GitLab 注册之后,由 GitLab 根据 .gitlab-ci.yml 里定义的 tag 和调度策略决定把哪个 Job 派发给哪个 Runner。搞清楚这三个角色的关系,后面写配置的时候就不会糊了。
2. 环境准备:GitLab部署与基础配置
2.1 Docker方式安装GitLab:关键参数逐个说清
GitLab 的部署方式有几种:Omnibus 包直接装、Docker 容器、Helm 上云。个人推荐中小团队直接用 Docker 方式,一是隔离干净,二是升级回滚都方便。我这次用的服务器是 8核16G,跑 GitLab CE 加两个 Runner 绰绰有余。
先给出我验证过的部署命令,再逐个解释参数:
sudo docker run -d \ --name gitlab \ --hostname gitlab.example.com \ -p 8083:80 \ -p 8443:443 \ -p 8022:22 \ -v /srv/gitlab/config:/etc/gitlab \ -v /srv/gitlab/logs:/var/log/gitlab \ -v /srv/gitlab/data:/var/opt/gitlab \ --restart always \ --shm-size 256m \ gitlab/gitlab-ce:latest几个容易踩坑的点我说一下。第一是端口映射,我这里把容器的 80 映射到宿主机的 8083,而不是直接用 80,原因是服务器上可能还有 Nginx 或别的 Web 服务,避免抢端口。第二是 hostname 参数,这会写进 GitLab 的对外 URL,如果后面用 SSH 克隆仓库,生成的地址里会有这个域名。如果你手头没有正式域名,也可以写成 IP 加端口的形式,比如gitlab.example.com换成192.168.1.10,但要注意把端口也写上。
启动之后第一次访问会比较慢,容器要初始化数据库和编译静态资源,我等过十分钟以上。你可以用docker logs -f gitlab观察服务是否就绪。首次访问会让你设置 root 密码,这个页面如果出现 422 错误,别慌,大概率是服务器时区或者浏览器缓存的问题,这一节最后我会专门讲修复方法。
安装完 GitLab 之后还有个高频问题,就是“GitLab 默认端口是多少”。默认情况下,Omnibus 安装会同时占用 80(HTTP)、443(HTTPS)、22(SSH)这三个端口,Docker 方式通过-p参数自定义映射后就没有固定说法了,你要看自己映射到哪。不少人装了之后发现 80 被占导致启动失败,其实改一下映射就行,不需要动 GitLab 内部配置。
2.2 SSH密钥与Access Token准备
代码仓库建好之后,第一步是让开发机能正常拉代码。GitLab 支持 HTTPS 和 SSH 两种方式,我个人强烈推荐 SSH,因为不用每次输入密码。而且 GitLab 里的 SSH key 体系很成熟,一个公钥可以授权给多个项目。
生成密钥的流程你应该很熟悉了:
ssh-keygen -t ed25519 -C "你的邮箱" -f ~/.ssh/id_ed25519生成完之后查看公钥:
cat ~/.ssh/id_ed25519.pub然后登录 GitLab,在右上角头像 → Preferences → SSH Keys 里粘贴公钥,保存即可。这里有一个经常被忽略的点:GitLab 会用你本地 git 配置里的用户名和邮箱去关联提交记录。如果本地的 user.name 和 user.email 跟 GitLab 账号不一致,你提交的代码会显示为“无法统计推送代码量”或显示成别人/未知作者。所以建议在所有开发机上统一执行:
git config --global user.name "你的名字" git config --global user.email "你的GitLab注册邮箱"另外说一下 Access Token 的获取方式。VSCode 里使用 HTTPS 克隆、调 GitLab API、或者某些 IDE 插件集成 GitLab 时,都会用到 Token。入口是头像 → Preferences → Access Tokens,勾选api、write_repository、read_repository这几个 scope,过期时间按需设置。生成的 token 只显示一次,一定要先复制保存再关页面。
2.3 让Pipeline跑起来之前:邮件通知与系统设置
很多人 Pipeline 写好了但收不到任何通知,跑挂了也不知道。GitLab 默认的邮件配置是关闭的,需要自己开 SMTP。我这里以常见的 163 邮箱或 QQ 邮箱为例,在/srv/gitlab/config/gitlab.rb里配置:
gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_address'] = "smtp.qq.com" gitlab_rails['smtp_port'] = 465 gitlab_rails['smtp_user_name'] = "你的邮箱" gitlab_rails['smtp_password'] = "邮箱的SMTP授权码" gitlab_rails['smtp_domain'] = "smtp.qq.com" gitlab_rails['smtp_authentication'] = "login" gitlab_rails['smtp_tls'] = true gitlab_rails['gitlab_email_from'] = "你的邮箱"改完之后执行docker exec gitlab gitlab-ctl reconfigure重启服务。注意很多邮箱需要单独开启 SMTP 服务并生成授权码,不是直接用登录密码。配置完之后你可以去用户设置里点一下“发送测试邮件”,能收到就说明没问题。这一步是小事,但做好了后面查问题会省很多时间。
3. 编写核心文件:.gitlab-ci.yml全解析
3.1 先从几个必须搞懂的关键字说起
.gitlab-ci.yml 是整个 CI/CD 的核心,语法不复杂,但有几个关键字的作用必须理解清楚,否则写出来的 Pipeline 会经常跑出匪夷所思的结果。
stages定义阶段列表,默认情况下每个阶段里的 job 会并行执行,不同阶段按顺序执行。job是真正干活的单元,一个 job 至少要包含script(要执行的命令)和stage(属于哪个阶段)。tags是 Runner 的标签,用于让 GitLab 决定把 job 分发给哪个 Runner,这个很多人会漏配,结果 Pipeline 一直卡在 pending。artifacts是用来保存构建产物的,它可以把当前 job 产生的文件传给后面的 job 使用,也可以直接在 GitLab 页面上下载。rules和only/except控制 job 在什么条件下执行,比如只在主分支上跑部署,在 MR 上只跑测试。
我见过太多人一上来就照着别人的完整模板抄,结果生产环境和测试环境共用同一个 Pipeline,一个不小心把测试包部署到了线上。正确做法是先搞清楚分支策略,再写 rules。比如main分支跑完整流程,develop分支只跑构建和测试,release/*分支跑预发布部署。用rules表达:
rules: - if: '$CI_COMMIT_BRANCH == "main"' when: always - if: '$CI_COMMIT_BRANCH == "develop"' when: always - when: never3.2 直接可用的完整Pipeline示例
以我最近做的一个前端项目为例,技术栈是 Vue3 + Vite + Nginx,目标是打包后推到一台业务服务器完成部署。完整 .gitlab-ci.yml 如下:
stages: - install - test - build - deploy variables: NODE_IMAGE: node:18-alpine cache: key: "$CI_COMMIT_REF_SLUG" paths: - node_modules/ install: stage: install image: $NODE_IMAGE tags: - docker-runner script: - npm config set registry https://registry.npmmirror.com - npm install artifacts: expire_in: 2 hours paths: - node_modules/ test: stage: test image: $NODE_IMAGE tags: - docker-runner script: - npm run lint - npm run test:unit dependencies: - install build: stage: build image: $NODE_IMAGE tags: - docker-runner script: - npm run build artifacts: expire_in: 2 hours paths: - dist/ dependencies: - install deploy: stage: deploy tags: - shell-runner script: - rsync -avz --delete dist/ deploy_user@$DEPLOY_SERVER:/var/www/project/ - ssh deploy_user@$DEPLOY_SERVER "sudo nginx -s reload" rules: - if: '$CI_COMMIT_BRANCH == "main"' when: manual dependencies: - build environment: name: production这份文件里有几个细节值得展开说。第一是cache和artifacts的区别:cache 用于加速依赖安装,比如 node_modules 缓存起来了,下次跑 install 就不用重新下载全部依赖;artifacts 是把关键产物显式传给下游 job。如果你不写dependencies,GitLab 默认会把前一个 stage 的所有 artifacts 都下载下来,项目大了之后非常浪费。显式声明依赖谁,只下载真正需要的东西。
第二是部署阶段的配置。我用了 manual 触发,也就是代码推到 main 分支之后,部署这一步不会自动执行,而是需要人工在 Pipeline 页面点一下“play”按钮。这是为了安全,毕竟线上部署很多时候需要一个确认动作,比如发版窗口到了才点。如果你希望完全自动化,把when: manual移除即可。
第三是安全考虑。rsync 和 ssh 通过密码验证非常难自动处理,所以我在目标服务器上配置了免密登录,把 Runner 所在机器的公钥放进了 deploy_user 的 authorized_keys。实际生产环境你还应该考虑用更严格的方式管理密钥,但作为快速起步,这种方式足够。
3.3 变量、锚点与缓存:让流水线更好维护
项目一多,你会发现自己写的 .gitlab-ci.yml 越来越长,大量重复的 script 段落很让人烦躁。GitLab CI/CD 支持 YAML 锚点,可以把公共脚本抽出来复用。比如:
.default_script: &default_script - echo "准备环境" - export NODE_ENV=production job1: script: - *default_script - npm run build更常用的是用variables定义全局变量,避免在多个 job 里重复写。前面例子里的NODE_IMAGE就是这样的玩法。如果你有某个 token 或密码,千万别直接写在 yml 文件里,要在 GitLab 项目的 Settings → CI/CD → Variables 里配成 masked 变量,然后以$VAR_NAME的方式引用。这样既安全又清晰,还能在多个项目间复用。
关于缓存还有一个建议:不要把缓存的 key 设得过于宽泛。我这个例子里的 key 是分支名,意思是每个分支有自己的 node_modules 缓存。如果你把 key 写死成一个字符串,所有分支共用缓存,一旦某个分支引入了破坏性依赖,别的分支也会跟着遭殃。
4. Runner的安装与调度策略
4.1 Executor怎么选:Shell还是Docker
Runner 是 Pipeline 的执行者,安装方式不难,难的是选择用什么 Executor。简单说,Executor 决定了你的命令跑在什么环境里。最常用的是 shell 和 docker 两种。
Shell Executor 最简单,Runner 直接在当前机器上执行命令,所有软件必须预先装好。它的优点是快、资源占用少,适合部署阶段使用,因为部署命令往往需要访问服务器上的 Docker 或 Nginx,直接在本机执行最方便。缺点是环境隔离差,如果多个项目共用一台 Runner,依赖经常互相污染。
Docker Executor 每个 job 会启动一个容器,在容器里跑命令。优点是用完即焚,环境干净,不同项目只要在 yml 里指定不同的image就能切换工具链。缺点是第一次拉取镜像耗时,而且部署阶段要在容器里执行 rsync、ssh 等操作需要额外配置。
我的习惯是:一个项目注册两个 Runner,一个 tag 为docker-runner,专门跑安装、测试、构建这类消耗型任务;另一个 tag 为shell-runner,专门跑部署任务。这样既保证了构建环境的干净,又让部署阶段能直接操作宿主机上的服务和密钥。下面我按这个思路给出安装注册步骤。
4.2 注册Runner的完整过程
先安装 GitLab Runner。我以 Ubuntu 为例:
# 添加官方源 curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash sudo apt-get install gitlab-runner然后注册。你需要先拿到 GitLab 的地址和注册 token。进入项目或群组的 Settings → CI/CD → Runners,就能看到注册地址和一个 token。这个 token 是专门给 Runner 注册用的,跟 API Token 不是一回事,别搞混了。
执行注册:
sudo gitlab-runner register过程中会问你几个问题:GitLab URL 填你访问 GitLab 的地址,比如http://192.168.1.10:8083;Registration token 填刚才看到的 token;描述随便写;Tags 这一步很关键,对应你在 yml 里写的 tags,比如docker-runner;Executor 类型选 docker;Docker image 填node:18-alpine。注册完验证:
sudo gitlab-runner list能看到刚注册的 Runner 状态为 online 就成功了。这里有个必须注意的点:如果你在 yml 里写了 tags,而 Runner 没有这个 tag,对应 job 会一直卡在 pending,因为它不知道该把活派给谁。所以要么 yml 不写 tags(极少用,不推荐),要么确保每个 Runner 都有匹配的 tag。
Runner 默认是单并发,也就是一次只能跑一个 job。如果你的团队提交很频繁,可以在/etc/gitlab-runner/config.toml里给 runner 设置concurrent = 4,然后sudo gitlab-runner restart。但要注意并发数提高之后服务器负载会明显上升,8G 内存的机器跑 4 个 node 构建任务基本到极限了,再往上就得考虑加机器或改成 Kubernetes 模式。
5. 常见问题与排查技巧实录
5.1 登录与Token类报错
先说说很多人一装完 GitLab 就遇到的 422 登录错误。现象是打开网页设置 root 密码时,提交后提示 422,或者登录的时候明明密码对了还是进不去。这种问题通常有三个原因:一是服务器时间和浏览器时间不一致,导致 cookie 校验失败,解决办法是同步服务器时间,或者换个浏览器试试;二是浏览器缓存了旧的 GitLab 页面,用隐身模式登录通常能绕过去;三是 Omnibus 初始化时 root 密码没有正确写入数据库,可以进入容器里用命令强制重置:
docker exec -it gitlab gitlab-rails runner "user = User.find_by(username: 'root'); user.password = '新密码'; user.password_confirmation = '新密码'; user.save!"然后是那个万恶的报错:login failed. check api token or gitlab version. log in via git if the versi。这个报错我排查了最久,最后定位到原因是 GitLab API token 失效了,或者本地 IDE/脚本里配置的 GitLab 版本号跟服务端实际版本不匹配。解决思路很清晰:去 Access Tokens 页面重新生成一个 token,确保 scope 包含了api;同时在客户端配置里检查 GitLab URL 和版本号是否和服务端一致。如果你用的是 VSCode 的 GitLab 插件,重新登录一次基本就好。
还有关于 Token 的另一个高频疑问:gitlab token在哪里。记住几个入口:用户级别的 Access Token 在头像 → Preferences → Access Tokens;项目级别的 Access Token 在项目 Settings → Access Tokens。权限范围不同,分别给个人操作和 CI 集成用。
5.2 Pipeline状态与权限类问题
Pipeline 卡在 pending 是最常见的调度问题。一般分两种情况:一是 Job 定义了 tags,但没有任何在线 Runner 带这个 tag;二是 Runner 在线但 executor 是 docker,而它对应的机器上没装 Docker 或拉不到镜像。第一种很隐蔽,因为 Runner 在 GitLab 页面显示 online,但如果你给它改了 tag 没重新注册,旧的配置不会自动同步。排查的时候打开 Pipeline 的 Job 详情页,GitLab 会明确告诉你“这个 Job 匹配不到 Runner”以及原因。
第二个常见问题是restore 时 报无权限。这是用 GitLab 备份恢复功能时特别容易遇到的,现象是在页面或命令行执行 restore 操作后,提示备份目录或文件没有访问权限。原因是备份文件在/var/opt/gitlab/backups下,属主不是 git 用户。解决办法:
sudo chown -R git:git /var/opt/gitlab/backups sudo chmod 700 /var/opt/gitlab/backups然后是 git 用户身份问题。很多人在 Runner 上用gitlab-runner用户执行脚本,结果遇到各种 permission denied。一个省事的方案是给 gitlab-runner 用户配 sudo 权限,注意要控制命令白名单,别把所有 root 权限都放开。或者直接把 Runner 的 user 改成 root(在 config.toml 里设置user = "root"),测试环境图省事可以这样,生产环境我强烈不建议,一旦脚本被注入恶意命令,等于直接拿到服务器管理权限。
5.3 部署与运维中的其他高频问题
再记几个我遇到过、网上也问得多的问题,都值得花五分钟提前解决。
问题一:GitLab 新建仓库在主页看不到。这通常是可见性设置的问题。创建项目时如果选了 Private,那只有项目成员能看到,外部用户或未登录账号自然是看不到的。如果你希望这个项目展示在群组主页或者个人主页,将项目的可见性改成 Internal 或 Public,或者确认你的账号是该项目成员,镜像冲突不太可能存在,因为你新建的仓库不是同一个。
问题二:VSCode 从 GitLab 拉项目到本地。VSCode 自带 Git 面板,直接用Ctrl+Shift+P调出Git: Clone,输入 GitLab 仓库页面上复制下来的 HTTPS 或 SSH 地址即可。HTTPS 方式首次会要求输入账号密码,密码其实就是刚刚那个 Access Token,这也是很多人输入登录密码却报错的原因——GitLab 在 13.x 以后就只接受 token 作为 HTTPS 方式的密码字段了。
问题三:Jenkins 和 GitLab 能否运行在同一台主机的 Docker 上。可以,但要注意两点:一是端口别冲突,两个 Web 服务会同时占用 80 或 443,需要把其中一个映射到别的端口;二是内存要足够,Jenkins 本身很吃内存,GitLab 官方建议至少 4G,两个叠一起 8G 起步比较稳。我的建议是如果可以分开尽量分开,如果只是临时测试,合在一起没问题。
问题四:GitLab 高危漏洞修复方案。这类问题本质上没有一劳永逸的答案,只能建一个规范流程:订阅 GitLab release 邮件,发现新版本后先在测试环境升级,确认没问题再升生产。Docker 方式升级特别简单,先 pull 新版本镜像,再重建容器,数据都在 volume 里不会丢,升级前记得手动备份。备份命令是:
docker exec -t gitlab gitlab-backup create问题五:分支合并策略。GitLab 合并分支到主分支时建议开启 MR 的管道检查,也就是合并请求里的 Pipeline 必须全部通过才能点 Merge。这个设置在项目 Settings → General → Merge requests 里打开。配合前面写的 rules,让 MR 只跑测试不部署,这就形成了一个很好的质量闸门。
最后分享一点个人体会
如果你以前习惯了手动发布、熬夜上线,第一次跑通 GitLab CI/CD 之后会明显感受到工作方式的变化:发布不再需要找运维排队,代码提交后自动完成大部分验证,失败的构建会精确告诉你是哪一步出了问题。但我想提醒一句,CI/CD 跑起来只是开始,真正考验团队的是怎么把流程规范好——谁可以触发部署、哪些分支必须走 MR、构建产物怎么归档、生产环境的密钥怎么管理。把这些问题想清楚,Pipeline 才是工程效率的加速器,而不是另一个需要维护的新系统。