做研发这几年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 安装会同时占用 80HTTP、443HTTPS、22SSH这三个端口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。如果你不写dependenciesGitLab 默认会把前一个 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_ENVproduction 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还是DockerRunner 是 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:8083Registration token 填刚才看到的 token描述随便写Tags 这一步很关键对应你在 yml 里写的 tags比如docker-runnerExecutor 类型选 dockerDocker 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 面板直接用CtrlShiftP调出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 才是工程效率的加速器而不是另一个需要维护的新系统。
