ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GitLab与Runner部署实战:内网CI/CD流水线从零搭建指南

GitLab与Runner部署实战:内网CI/CD流水线从零搭建指南 在中小型团队里“Gitlab和runner部署”几乎是可以贯穿整个研发流程的标杆工程——一个是代码托管和CI/CD的控制中枢一个是真正去跑流水线任务的执行单元两者配合起来才能在内网环境下形成一套完整的DevOps闭环。我见过不少项目死在半路上GitLab装好了没考虑runner的选型或者runner注册之后套件和标签对不上最后流水线一直pending。这篇文章我会把GitLab社区版和GitLab Runner从零开始讲透覆盖前期规划、Docker部署细节、runner注册、流水线落地以及排查过程中那些踩过的坑。整套部署方案的受众其实很广刚接手团队内部代码平台的运维新人需要自建代码管理系统的后端小伙伴甚至想把个人项目放在内网服务器上做自动构建的独立开发者都可以照着本文走一遍。看完之后你至少能自己搭起一套能跑的GitLab环境然后把第一个流水线任务跑通。1. 部署方案选型与资源规划1.1 为什么优先选择Docker部署GitLab关于GitLab的部署方式官方提供了Omnibus包、源码安装、云镜像和容器镜像几种路径。对于绝大多数场景我推荐直接用Docker方式理由并不神秘——GitLab依赖的服务非常多包括PostgreSQL、Redis、Gitaly、NGINX等Omnibus包虽然解决了依赖问题但升级和回滚都绑死在系统包管理上一旦操作系统版本变化迁移很麻烦。而Docker镜像把这些组件都封装进同一个容器里环境隔离做得干净备份和迁移也只需操作两个目录。基于我在二三十台机器上的部署经验Docker方式确实省心。唯一的例外是那种特别在意裸机性能、或者需要深度定制GitLab内部模块的团队那种场景下Omnibus包反而更顺手。但日常的内网部署我建议直接拥抱容器化。GitLab官方对Docker的支持很成熟社区版和企业版镜像都维护得很及时甚至连版本升级都提供了官方文档。资源规划上需要提前打个底GitLab虽然功能丰富但它的内存占用是出了名的“不低”。一台2核4G的云主机跑社区版如果团队人数在20人以内日常使用问题不大但跑CI构建时会有明显压力。官方建议是至少4GB内存运行生产环境同时把交换分区配置出来避免高峰期OOM。磁盘方面代码本身占用不高但仓库的.git目录、容器镜像、构建缓存会持续增长建议预留200GB以上并定期清理。1.2 端口规划与域名设定部署前先把端口和域名规划好这一步一旦定了想改很麻烦。GitLab默认会用80端口提供Web访问但如果你在同一台服务器上还要跑其他Web服务端口就要错开。比较稳妥的做法是这样设置宿主机SSH端口保持22如果是云服务器GitLab的容器SSH端口映射到宿主机2222或1022避免和宿主SSH冲突。Web端口映射到宿主机8080或者8800然后在NGINX或DNS层面做一层转发。HTTPS如果内网有统一的证书体系可以启用TLS否则先以HTTP跑通再补证书。域名部分我习惯用一个独立域名或二级域名比如gitlab.company.local然后通过/etc/hosts或者内网DNS解析到服务器IP。这种做法的好处是后续迁移服务器时不需要改动客户端的remote地址只需要改DNS记录就行。如果你图省事直接用IP端口访问也可以只不过每次克隆代码时都要带上端口号体验稍差。2. Docker部署GitLab的完整实操2.1 用一条清晰的Docker命令把容器拉起来部署GitLab的容器化步骤本质上就是把官方镜像跑起来并且把关键目录挂载出来。我用的命令基本是下面这套实际部署时根据服务器情况调整参数即可export GITLAB_HOME/srv/gitlab sudo docker run --detach \ --hostname gitlab.example.com \ --publish 8443:443 \ --publish 8080:80 \ --publish 1022:22 \ --name gitlab \ --restart always \ --volume $GITLAB_HOME/config:/etc/gitlab \ --volume $GITLAB_HOME/logs:/var/log/gitlab \ --volume $GITLAB_HOME/data:/var/opt/gitlab \ --shm-size 256m \ gitlab/gitlab-ce:latest每个参数都有自己的意义我挑重点解释一下。--hostname会写进GitLab的配置文件最终生成的项目克隆地址都会以这个主机名为基础所以不要随便填。--publish那三个端口分别对应容器的HTTPS、HTTP和SSH服务宿主机的映射可以根据你的规划改。--shm-size是给容器共享内存的配额PostgreSQL和Gitaly对共享内存有依赖给太小会导致奇怪的性能问题或启动异常。挂载目录是这套部署的核心。GitLab的配置、日志、数据都存放在容器内容器一旦删除如果没有挂载数据就全没了。你把它们分别映射到宿主机的config、logs、data这三个目录后续备份、升级、迁移都只需要操作这些目录。第一次启动image拉取时间较长几百MB到1GB不等耐心等待即可。启动完成后容器内部需要一段时间初始化数据库和编译资源一般3到5分钟。你可以通过下面的命令观察状态sudo docker logs -f gitlab看到类似GitLab is ready或者日志里出现http://gitlab.example.com的提示说明服务已经可以访问了。2.2 初始化配置与首次登录注意事项第一次打开GitLab页面浏览器会强制跳转到密码设置界面这里要设置root管理员账号的初始密码。这里有一个很多人都会踩的坑如果你没用上面的方式设置密码GitLab会在/etc/gitlab/initial_root_password文件中自动生成一个临时密码这个文件在容器重新配置或者24小时后会被自动清理。如果你在页面上看到无法登录先去看这个文件sudo docker exec -it gitlab cat /etc/gitlab/initial_root_password拿到密码登录后第一件事就是把root密码改成自己的强密码并开启两步验证。在管理后台里建议先把Sign-up enabled关掉也就是禁止自助注册否则任何人都可以自己注册一个账号这在团队内部是不安全的行为。然后把项目的可见性改成Private默认应该是Internal建议关闭公开访问。还有一个很重要的细节是修改SSH端口配置。因为容器内部的SSH端口是22外部映射成了宿主机1022GitLab默认生成的克隆地址还是会用22端口导致用户克隆代码时连不上。解决方法是在配置文件/etc/gitlab/gitlab.rb中显式指定gitlab_rails[gitlab_shell_ssh_port] 1022然后执行gitlab-ctl reconfigure。这样项目页面上展示的SSH克隆地址就会自动带上:1022。2.3 项目导入与SSH密钥配置把已有代码导入GitLab常见的做法有两种直接命令行推送或者通过界面导入。命令行方式适合代码已经在本地并且你熟悉git操作的场景。假设你本地已经有一个项目目录想要推到GitLab的新仓库先需要在GitLab上创建一个空项目拿到仓库地址然后执行git init git remote add origin gitgitlab.example.com:group/project.git git add . git commit -m Initial commit git push -u origin master如果项目是从GitHub或码云迁移过来的GitLab提供了导入工具在“新建项目”里选择“Import project”填写源仓库的地址和认证信息即可它会自动拉取远端仓库的提交历史和分支信息整个过程不需要在本地做额外操作。SSH密钥的配置也经常出问题。开发者本地生成密钥对把公钥粘贴到GitLab的Preferences - SSH Keys中然后把私钥添加到本地ssh-agent。这里我要强调一个细节如果你在本地配置了多个SSH密钥用于不同的代码平台务必在~/.ssh/config里为不同host指定不同的IdentityFile否则git会默认使用第一个密钥连接GitLab时极大概率会报Permission denied (publickey)。我曾经因为这个原因排查了大半天最后发现是全局agent加载了旧密钥。3. Runner的安装、注册与场景选型3.1 共享Runner和特定Runner怎么选Runner是GitLab CI/CD的执行者。GitLab CI流水线定义好之后由Runner去实际拉取代码、执行任务脚本、上传产物和报告。Runner本身有三种作用域共享Runner、群组Runner和项目Runner。共享RunnerShared Runner注册到整个GitLab实例所有项目都可以使用适合中小企业内部做统一构建资源池。缺点是如果某个项目的构建脚本特别耗资源会挤占其他项目的资源而且共享Runner的权限较大建议只给信任的项目开启。群组RunnerGroup Runner注册到某个群组下该群组内所有项目都可以使用适合按业务线划分的团队。特定RunnerSpecific Runner只服务于绑定的项目资源隔离效果好最适合对构建环境有特定要求的项目。从团队协作效率角度我一般建议先配一个共享Runner跑常规任务再为一些重量级项目配置特定Runner这样既保证通用任务有资源可跑又可以防止特殊的构建任务互相污染。3.2 Runner安装与注册步骤GitLab Runner支持二进制安装、Docker安装和Helm部署最简单的就是二进制安装。以常见的Linux服务器为例# 下载二进制文件版本号可根据实际情况调整 sudo curl -L --output /usr/local/bin/gitlab-runner \ https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 # 赋予执行权限 sudo chmod x /usr/local/bin/gitlab-runner # 创建GitLab Runner用户 sudo useradd --comment GitLab Runner --create-home gitlab-runner --shell /bin/bash # 安装并启动服务 sudo gitlab-runner install --usergitlab-runner --working-directory/home/gitlab-runner sudo gitlab-runner start注册Runner时需要到GitLab的管理后台Admin Area - CI/CD - Runners这里能看到注册地址和注册令牌。注册命令如下sudo gitlab-runner register \ --url http://gitlab.example.com \ --registration-token 你的令牌 \ --executor docker \ --docker-image alpine:latest \ --description shared-runner-docker \ --tag-list docker,shared \ --run-untaggedtrue \ --lockedfalse--executor docker这一项尤其关键。Runner在执行任务时会有自己的执行器类型包括shell、docker、kubernetes等。用Docker执行器每一个CI任务都会在一个全新的Docker容器里运行环境完全隔离不会因为上一个构建留下了脏文件而影响下一次构建。--docker-image指定了默认镜像后续在流水线里可以按需覆盖。--tag-list用于给Runner打标签项目流水线里会通过tags关键字来匹配可以使用的Runner。需要说明的是registration-token这个注册方式在较新版本的GitLab中逐渐被弃用替代方案是先在界面上创建Runner然后使用它的Authentication Token进行注册但思路是一致的就是拿着凭证去让Runner“认领”任务。3.3 Runner执行器的选择与资源限制如果你只在一台服务器上部署GitLab和Runner我建议Runner的执行器选择shell模式而不是Docker模式。原因很简单Docker执行器会拉取镜像、创建容器对内存和CPU的额外开销不小。而shell执行器直接在宿主机上执行命令速度快资源占用低。但是shell执行器有一个明显的短板——环境依赖会互相污染。比如项目A的构建脚本要先设置Java 8项目B需要Java 17如果都用shell执行器你就要在宿主机上维护两套JDK并切换环境变量非常烦琐。这种情况下Docker执行器的隔离优势就体现出来了每个项目在各自的容器里构建互不干扰。Runner的并发数也需要根据机器配置来限制。/etc/gitlab-runner/config.toml里有两个核心参数一个是总的concurrent一个是每个Runner的limit。比如你的服务器是8核16G我可以把concurrent设为4也就是最多同时跑4个任务避免一次开太多容器导致服务器卡死。这个参数没有标准答案要结合任务的CPU密集程度来调整一个稳妥的判断方式是看任务高峰期系统负载load average持续超过CPU核数的时候就要调低并发。4. 编写并跑通第一条CI/CD流水线4.1 .gitlab-ci.yml的核心结构与关键字Runner就绪之后CI/CD的核心就转移到了仓库根目录下的.gitlab-ci.yml文件。GitLab的流水线定义完全围绕这个YAML文件展开它位于仓库根目录时每次push到指定分支都会触发流水线。我见过很多初次接触的人以为这个文件需要去后台配置其实不是它就是代码仓库里的一个普通文件跟着代码一起提交。一个最简的流水线定义长这样stages: - build - test - deploy build-job: stage: build tags: - docker script: - echo Compiling the code... - make build test-job: stage: test tags: - docker script: - echo Running tests... - make test deploy-job: stage: deploy tags: - docker script: - echo Deploying application... - deploy.shstages定义了流水线的阶段顺序默认情况下三个阶段是串行执行的build通过了才会跑testtest通过了才会跑deploy。每个job里面的script是必须字段里面包含的每一条命令会依次执行。如果某一条命令返回非零退出码整个job会被标记为failed流水线也会中止这一点和shell脚本的执行逻辑完全一样。tags字段是用于匹配Runner的。上面注册Runner时如果打了docker这个标签那么任何带tags: [docker]的job都会被这个Runner接收。这里有个经典的坑如果你的Runner设置了标签但job没有写tags字段或者写了不存在的标签job会一直卡在pending状态因为它找不到匹配的Runner来执行。4.2 一个带有缓存和产物的Java项目流水线示例为了更贴近实际我以一个常见的Java Maven项目为例写一个带缓存和产物的流水线stages: - build - test - package variables: MAVEN_OPTS: -Dmaven.repo.local.m2/repository cache: paths: - .m2/repository/ before_script: - echo Pipeline started at $(date) maven-build: stage: build image: maven:3.8-openjdk-11 tags: - docker script: - mvn compile maven-test: stage: test image: maven:3.8-openjdk-11 tags: - docker script: - mvn test artifacts: paths: - target/surefire-reports/ expire_in: 7 days maven-package: stage: package image: maven:3.8-openjdk-11 tags: - docker script: - mvn package -DskipTests artifacts: paths: - target/*.jar expire_in: 30 days这段配置里cache关键字设置了Maven的本地仓库缓存路径这样多个job之间可以共享依赖下载结果显著加快构建速度。artifacts用于保存job产生的文件比如测试报告和构建产物它们可以被后续阶段的job下载也可以直接在GitLab界面上下载存档。expire_in用来控制过期时间避免产物永久占用磁盘。还有一个非常实用的小操作在job里可以设置rules来控制流水线在什么情况下运行。比如我只希望master分支的push触发部署可以用下面这种写法deploy-prod: stage: deploy tags: - docker rules: - if: $CI_COMMIT_BRANCH master when: manual script: - ./deploy.sh这样在master分支上才会出现部署任务而且是需要手动点击触发的适合生产环境发布的场景。when: manual把自动触发改成了手动确认多了一层防呆设计对生产环境的保护非常明显。4.3 Runner注册完“运行中”的标志怎么看在GitLab界面上确认Runner是否真正可用有几个地方要看。第一个是Admin Area - CI/CD - Runners页面Runner列表里应该能看到你注册的那台机器状态显示为绿色并且Online字段是正常的如果是灰色的Offline状态说明Runner进程没有连上GitLab检查Runner机器和GitLab服务器之间的网络以及服务是否启动。第二个地方是某个项目的CI/CD - Pipelines页面当你Push代码后这里会出现一条流水线记录。如果一切正常它的状态会从pending变成running再变成passed。如果一直停在pending最有可能的原因就是job里写的tags匹配不到在线Runner。第三个地方是job的日志。点进流水线里的每个job可以实时看到Runner执行命令的输出。如果job失败日志里会直接显示是哪条命令返回了非零退出码这是排查问题最快的信息源比猜原因高效得多。5. 常见问题与排查技巧实录5.1 高频故障速查表故障现象主要原因排查方法页面访问不了容器还在初始化查看docker logs -f gitlab等待就绪克隆地址端口不对未设置gitlab_shell_ssh_port修改gitlab.rb后执行gitlab-ctl reconfigure流水线一直pendingjob的tags匹配不到Runner检查Runner标签和job的tags是否一致Runner状态Offline服务未启动或网络不通执行gitlab-runner status检查构建时连不上GitLab仓库Runner和GitLab间存在网络隔离确认Runner所在网络可访问GitLab的IP和端口容器频繁重启内存不足或配置错误调整内存、启用swap、查看docker inspect日志项目clone时报权限错误SSH密钥未配置或错误使用ssh -T gitgitlab.example.com测试连通性这张表基本覆盖了并测过程中的大多数问题。遇到问题不要慌按照从简单到复杂的顺序排查先看服务状态再看日志最后才去怀疑配置问题。5.2 排查技巧日志、配置与命令三件套GitLab的日志系统是排查问题的第一助手。容器内的日志主要分布在/var/log/gitlab我常用的是这几个# 查看生产环境日志 sudo docker exec -it gitlab gitlab-ctl tail production.log # 查看NGINX访问日志 sudo docker exec -it gitlab gitlab-ctl tail nginx/access.log # 查看Sidekiq后台任务队列日志 sudo docker exec -it gitlab gitlab-ctl tail sidekiqGitLab的很多功能是异步处理的比如项目导入、合并请求的CI触发等都依赖Sidekiq。如果Sidekiq卡住用户看到的结果就是某些操作没有回应。看到Sidekiq相关的异常日志优先检查数据库连接和内存这两个是Sidekiq最容易出问题的地方。Runner的日志排查相对简单主要是查看服务状态和实际执行日志sudo gitlab-runner status sudo gitlab-runner --debug run-single 21 | tee /tmp/runner-debug.log--debug run-single这种方式可以单独启动一个调试Runner进程把注册信息显式输出方便定位注册和连接问题。我自己遇到Runner反复断连的情况时会把并发调低然后再跑一次排查是否是资源问题导致的僵死这个方法屡试不爽。5.3 内存不足和磁盘膨胀的处理方案GitLab的三个容器目录里数据目录增长最快的是data。其中gitaly存放Git仓库本身postgresql是数据库gitlab-ci下的shared目录存放构建产物和流水线缓存。时间久了这些目录的体积会非常可观可以用下面的命令快速看占用sudo du -sh /srv/gitlab/data/* | sort -hr | head -20针对缓存和产物膨胀可以在GitLab管理后台设置全局的Artifacts保留周期比如默认保留30天。同时容器内的临时文件也可以定期清理sudo docker exec -it gitlab gitlab-ctl cleanup-cache内存不足是另一个高发问题。Linux服务器上Cloud OOM时容器会被直接杀掉表现就是docker ps看不到GitLab容器或者容器一直重启。我建议在/etc/gitlab/gitlab.rb里明确限制几个核心组件的内存使用典型配置如下puma[worker_processes] 2 sidekiq[max_concurrency] 5 postgresql[shared_buffers] 256MB gitaly[cgroups_count] 4这些值要按机器内存来调整。设置完成后记得gitlab-ctl reconfigure。这一步能明显降低OOM概率尤其是4G内存的机器上效果立竿见影。6. 部署完成后的加固、备份与维护建议6.1 备份恢复的最佳实践GitLab自带的备份工具是我目前用过最省心的方案之一它会把数据库、仓库、上传文件打包成一个tar文件。只要服务器上留出足够磁盘定时执行备份非常简单。容器内执行备份命令如下sudo docker exec -it gitlab gitlab-backup create备份文件默认生成在/var/opt/gitlab/backups由于我们已经挂载了data目录所以它实际落在宿主机的/srv/gitlab/data/backups下。为了保险我还会用rsync把备份文件同步到另一台机器或者对象存储防止整机故障导致备份丢失。恢复时需要注意GitLab版本的一致性——用不同版本的备份文件去恢复极容易报错。恢复的命令是sudo docker exec -it gitlab gitlab-ctl stop puma sudo docker exec -it gitlab gitlab-ctl stop sidekiq sudo docker exec -it gitlab gitlab-backup restore BACKUP备份文件名 sudo docker exec -it gitlab gitlab-ctl start恢复前先停掉应用服务是为了避免数据库写入冲突。很多人第一步没停服务结果恢复完数据不一致这个顺序很重要。除了数据备份配置文件/etc/gitlab/gitlab.rb也要定期备份它决定了整个GitLab实例的对外行为和资源参数丢失之后重新配置非常痛苦。6.2 版本升级的正确姿势GitLab的升级策略不像某些软件可以跨大版本直接跳。官方建议的路径是一个大版本一个大版本地往上升比如14.x升到15.x再升到16.x。跨版本升级极容易出现数据库迁移失败这是GitLab升级里最坑的地方。所以升级前先查看自己的当前版本和目标版本GitLab官网有升级路径图照着走就行。具体的升级操作无非是拉取新镜像、停止旧容器、用相同挂载方式重新创建容器sudo docker pull gitlab/gitlab-ce:16.x.x-ce.0 sudo docker stop gitlab sudo docker rm gitlab # 使用和创建时相同的命令重新运行容器但指定新镜像升级过程中容器启动后会自动执行数据库迁移所以第一次启动时间会比较长日志会显示Migrating database之类的信息此时不要强行中断否则数据库可能处于不一致状态。升级完第一时间访问页面确认主要功能正常再去跑流水线。6.3 安全加固的几个最小操作清单GitLab一旦开放到内网或公网安全加固就变得重要。我不主张做复杂的配置但有几个基础动作必须做关闭公开注册。管理后台里取消Sign-up enabled勾选杜绝随意注册。启用Docker容器的自动更新策略。镜像有更新时及时跟进GitLab几乎每个月都会发布安全版本补丁速度比非安全版本快得多。设置访问控制。把GitLab管理端只开放给运维人员的IP段通过防火墙限制443或8080端口的来源IP。设置统一的强密码策略。管理后台可以配置密码最小长度和复杂度很多人嫌麻烦但密码策略是成本最低的安全防线。开启SSH密钥过期设置。GitLab可以给钥匙密钥设置过期时间避免离职员工的密钥长期有效。6.4 给我的经验补一句部署GitLab和Runner这套系统技术上并不复杂但真正的难点在于把“部署”变成“长期稳定运行”。我把大部分时间花在了监控、备份、资源调优和升级上GitLab本身反而是最稳定的一环。实际维护过程中GitLab和Runner之间的配合比想象中有更多细节比如标签命名规范、Runner的并发控制、镜像版本锁定这些都会影响流水线的稳定程度。如果你刚完成部署短期内先不要追求复杂的CI/CD流程先把最简单的三个job跑通——检查代码能不能通过编译、测试能不能通过、部署脚本能不能执行。等这条路径稳定了再逐渐加入镜像构建、产物归档、通知机器人等扩展功能。这比一开始就设计一个庞大的流水线结果遇到问题不知道从哪排查要稳妥得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进