在企业里干过几年运维和DevOps的人,估计都经历过这种状态:代码还在用网盘传,构建靠某个同事本地IDEA跑,镜像用tar包人肉拷到服务器,发布要等发版群里吼一声才动手。我接手的那套业务系统就是这个样子,每周发版一次,全组人忙活两小时,还经常因为构建环境不一致出现“我机器上明明好好的”这种话。后来我花了几周时间,把流程重写了一遍:用Docker作为统一运行底座,把GitLab、Jenkins、Harbor三件套全部容器化部署起来,组成了一套基于Docker容器的DevOps应用方案。这篇文章就是这次落地的完整记录,包括选型逻辑、部署细节、流水线跑通,以及运行半年后沉淀下来的运维经验。打算在公司里自建CI/CD平台、准备用容器化方式管理代码发布链路的朋友,可以参考这条路径。
1. 为什么是这三件套:企业代码发布系统的选型逻辑
1.1 企业业务发布最典型的四个痛点
先说我接手时的真实状态。代码管理用的是网盘共享目录,分支基本靠文件夹名字区分;构建完全依赖个人电脑,JDK版本、Maven仓库、Node版本全凭自觉;发布方式是把war包或jar包传到服务器上手动重启。这四个环节凑在一起,任何一个出问题,都能让发版推迟半天。
后来我们痛定思痛,决定上一套标准化的代码发布系统。核心诉求其实就四条:代码有统一管理、构建能自动触发、镜像有私仓存储、发布有完整记录。对应到技术栈上,就是典型的GitLab加Jenkins加Harbor。
1.2 三个组件各管一段
GitLab管代码仓库,承载开发同学日常的提交、分支、Merge Request、代码评审。Jenkins管流水线调度,从GitLab拉代码、跑测试、构建镜像,再把镜像推到仓库。Harbor管镜像的存储和分发,生产服务器只需要从Harbor拉取指定版本的镜像,就能实现可重复、可回滚的发布。
这三个东西单独看都不是新东西,但组合在一起,就形成了企业业务代码发布的最小闭环:开发提交代码,Jenkins自动构建出镜像,Harbor保存所有版本,服务器随时拉取任意版本实现发布或回滚。
1.3 为什么坚持用Docker容器化部署
当时有一个备选方案是直接在物理机上装GitLab、Jenkins和Harbor的二进制包。我没有选它,原因很实操:这套系统本身也需要升级、迁移、备份。如果用传统方式部署,每次换机器、换版本,都要重新处理依赖环境,一台机器上的配置改坏了,整台机器都要陪葬。容器化之后,所有配置都写在docker-compose.yml和挂载目录里,迁移就是拷贝目录、换个机器、重新docker-compose up。
我把两种方式的对比整理过一张表:
| 对比项 | 传统物理机部署 | Docker容器化部署 |
|---|---|---|
| 部署新机耗时 | 每台约半天,涉及依赖安装 | 拷贝数据目录加一条命令,约半小时 |
| 环境一致性 | 依赖手工记录依赖版本 | 镜像固定,版本可复现 |
| 升级回滚 | 升级失败难回滚 | 镜像tag切换即可回滚 |
| 资源隔离 | 进程间互相影响 | 容器间资源可控 |
| 备份恢复 | 依赖系统快照或整机备份 | 备份挂载目录即可 |
| 多实例扩展 | 需要重复部署环境 | 复制容器编排文件即可 |
这套组合跑下来,我的结论是:容器化并不是银弹,GitLab这种复杂应用容器化后依然要细心伺候数据目录,但对企业内部系统来说,可移植性和备份恢复的价值已经足够大。
2. GitLab容器化:社区版部署、企业配置与502问题的应对
2.1 用docker-compose统一管理,别用一句docker run硬扛
我见过不少人部署GitLab就是一条docker run丢上去,后面要加参数、改配置时痛苦得要命。GitLab配置项极多,最佳实践是写一个docker-compose.yml统一管理。我用的版本是gitlab/gitlab-ce的长期支持版,示例配置如下:
services: gitlab: image: gitlab/gitlab-ce:16.4.3-ce.0 container_name: gitlab hostname: gitlab.example.local restart: always environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.example.local:8929' gitlab_rails['gitlab_shell_ssh_port'] = 5222 ports: - "8929:8929" - "5222:22" volumes: - ./gitlab/config:/etc/gitlab - ./gitlab/logs:/var/log/gitlab - ./gitlab/data:/var/opt/gitlab shm_size: '256m'注意几个关键点。第一,external_url必须写用户实际访问的地址,如果外部通过8929端口访问,URL里就必须带上端口,否则页面里的项目克隆地址全是错的,点击克隆URL会少一个端口直接打不开。第二,容器内的22端口不能直接映射到宿主机22,因为宿主机的SSH通常已经被占用,所以映射成5222,同时通过gitlab_rails['gitlab_shell_ssh_port']告诉GitLab“对外SSH端口是5222”,否则生成的项目SSH地址会指向22。
2.2 三个挂载目录各管什么
GitLab容器化之后最怕的就是重启后数据丢光。我见过有人把GitLab的数据放在容器内部,容器一删就傻眼。正确做法是把三个目录全部挂载出来:
- /etc/gitlab 存放gitlab.rb配置文件
- /var/log/gitlab 存放日志
- /var/opt/gitlab 存放仓库数据、数据库、PostgreSQL数据
这三个目录分开挂载是有讲究的。配置文件独立挂载,改配置不需要进容器操作;日志独立挂载,排查问题时直接看宿主机文件就行;数据目录独立挂载,备份时只需要打包gitlab_backup目录和这几个挂载目录。我把三个目录放在同一个项目目录下,整个GitLab的数据就是自包含的一整块,随意搬走。
2.3 企业使用配置:新项目流程、SSH密钥与批量导入
GitLab部署起来只是第一步,团队真正用起来还需要配好日常操作流程。
首次访问时,root密码在容器初次启动后自动生成,可以通过docker logs grep密码或者直接进入容器用gitlab-rake命令重置:docker exec -it gitlab gitlab-rake "user:update_root_password"。
开发同学接入时最常用的是SSH密钥。在个人电脑上生成一对密钥,把公钥贴到GitLab的SSH Keys页面。因为部署时SSH端口映射到了5222,需要在git clone时将端口写进SSH配置,否则默认走22端口会连不上。
如果是老项目迁移,GitLab也支持批量导入。把旧仓库以裸仓库方式推入新地址就行,或者用GitLab自带的导入功能连接旧仓库批量拉取。
新项目流程也很简单:在GitLab里创建Project,选好项目名、可见性,然后把本地代码推上去。要注意的是,如果External URL配了端口,推代码时远程地址的端口也要一致,否则git push会被拒绝。
2.4 内存不足导致502:GitLab容器化最大的坑
GitLab是这三件套里最吃内存的。官方建议8GB内存,很多人拿一台4GB的机器就开跑,结果启动半小时后一访问,502。
我排查过一次502问题:刚启动时GitLab能访问,过几分钟就502,查看gitlab status发现postgresql或gitaly进程起不来或不断重启。看日志经常是Out of memory。解决办法有两个:一是加大宿主机内存,或者给容器配swap;二是调整GitLab自带的组件配置,比如用下面这段配置限制sidekiq和gitaly的开销:
# GITLAB_OMNIBUS_CONFIG 中追加 postgresql['shared_buffers'] = '256MB' sidekiq['max_concurrency'] = 5 puma['worker_processes'] = 2 gitaly['internal_socket_dir'] = '/var/opt/gitlab/gitaly'还有一个小技巧:GitLab官方镜像对共享内存要求较高,不设置shm_size参数的话,容器默认的/dev/shm只有64MB,跑一些大仓库操作时容易触发SegmentFault。我在compose文件里加了shm_size: '256m',这个问题就没再出现过。
2.5 别忽略GitLab的安全更新
GitLab是网络攻击的重点目标,官方每季度都会发布安全版本,修复一些远程代码执行或权限绕过类漏洞。我的做法是每月检查一次GitLab release notes,确认有新版本后,先在测试环境做备份再升级容器。升级本身不复杂,把镜像tag改成新版,重启容器即可。但一定要先备份数据目录和GitLab自带备份文件,我见过升级中途失败导致仓库数据损坏的事故,没有备份就只能从头建仓库。
3. Jenkins容器里的调度中枢:挂载Docker与主从Agent的实践
3.1 Jenkins主从架构的定位
Jenkins在这套系统里扮演的是“调度中枢”,它负责监听代码变化、执行构建、推送镜像。从设计上,我推荐主节点只跑Web控制台和流水线调度逻辑,真正耗时的构建任务交给Agent节点。如果主从都跑在同一个容器里,构建任务一多,Jenkins控制台都会卡顿。
但企业内部初期代码量不大的时候,单节点完全够用。拆Agent可以等团队并行构建需求出现之后再做,这也是渐进式改造的思路。
3.2 容器内调用Docker命令:挂载socket是最直接的方案
Jenkins容器本身不带Docker CLI,构建时需要宿主机的Docker帮它完成build和push操作。有两种常见方案:一是容器内装一个Docker客户端,通过网络上连到宿主机Docker;二是直接把宿主机的/var/run/docker.sock挂载进容器。
我用的第二种。原因很简单:配置最少,性能最好,不需要额外开端口。在docker-compose.yml里加上下面几行:
services: jenkins: image: jenkins/jenkins:lts-jdk17 container_name: jenkins ports: - "8090:8080" - "50000:50000" volumes: - ./jenkins_home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker挂载socket意味着容器内的Jenkins可以直接操作宿主机的Docker守护进程,这在逻辑上是“把整个Docker交给了Jenkins”。安全性上要谨慎,我不建议把这个能力授予不可信任务的Job,企业内部可控范围内用起来倒是很爽。
实测下来还有一个细节:宿主机用二进制安装的Docker和容器内的Docker CLI版本最好一致,否则会遇到client和server版本不兼容的告警,虽然大多数情况下构建还是能跑,但遇到特定API差异时会失败。
3.3 初始登录、汉化与国内镜像源配置
Jenkins首次启动后,初始密码在容器日志里:docker logs jenkins就能看到。跑起来之后第一件事,安装两个必备插件:GitLab插件用于连接GitLab和触发webhook;Pipeline插件用于定义流水线。
其次就是汉化。很多国内团队第一反应是装汉化插件,Jenkins插件中心有“Localization: Chinese (Simplified)”插件,装上后大部分界面会变中文。注意这插件名字是英文的,不要搜错。
插件下载速度慢是在国内部署Jenkins最常见的痛。解决方法是把插件更新地址切换到国内可用镜像源。在Manage Jenkins > Plugin Manager > Advance > Update Site里,把默认的update-center.json换成国内镜像地址。注意,这个配置只影响插件中心的列表和下载,不影响Jenkins插件本身的使用。
3.4 Jenkins里最常用的环境变量
写Jenkinsfile时,有一批内置环境变量是必须知道的。我刚用那会儿总是去控制台手动看环境变量列表,后来摸索出几个最常用的:
- BUILD_NUMBER:当前构建的序号,非常适合充当镜像tag
- JOB_NAME:当前任务名
- WORKSPACE:任务的工作目录
- GIT_COMMIT:当前构建对应的commit哈希
- BUILD_URL:当前构建在Jenkins上的链接
这些变量在写Pipeline时可以直接引用,比如构建镜像时用BUILD_NUMBER做tag,发布后可以在企业微信通知里带上BUILD_URL方便同事查看构建日志。
3.5 主从Agent的扩展思路
当团队并发构建需求上来之后,我补充了一套Agent策略。主节点保持轻量,Agent可以用单独的容器启动,注册到主节点上。Jenkins的官方镜像就提供了Agent的启动方式,主从之间通过TCP端口50000通信。
Agent节点同样需要挂载docker.sock才能构建镜像,多个Agent共享宿主机的Docker能力。这个模式下,主从都容器化,唯一的运维成本是确保Agent和主节点的Jenkins版本兼容,尽量全部用同一个jdk17镜像tag。
4. Harbor私有仓库落地:从HTTP起步到内网镜像分发的关键配置
4.1 为什么不用Docker Registry裸仓库而选Harbor
企业私有镜像仓库,裸的docker registry也能用,但那只是“能存镜像”而已。Harbor在registry之上补齐了企业需要的很多东西:项目隔离(按团队划分仓库空间)、访问控制(用户和机器人账号)、镜像漏洞扫描(能扫出基础镜像中的中高危漏洞)、镜像复制(做双机房同步)、操作审计日志。
实际使用中,我最常用的是“项目”和“机器人账号”这两个能力。开发团队各分一个项目,发布时用机器人账号做自动化推送,不用把管理员密码暴露在CI流水线里。
4.2 Harbor部署:centos7环境下的条件与步骤
Harbor的老版本部署依赖docker-compose,需要提前装好。装Harbor之前,先把docker和docker-compose准备到位,python3和openssl也必不可少,因为Harbor的install.sh会做环境检查。
Harbor提供离线安装包,下载后解压,核心就是修改harbor.yml。我这边的配置类似这样:
hostname: harbor.example.local http: port: 5080 https: port: 5443 certificate: /data/ssl/harbor.pem private_key: /data/ssl/harbor-key.pem harbor_admin_password: 改成高强度密码 database: password: root123 data_volume: /data/harbor第一次搭的时候我没配HTTPS,直接用HTTP跑。为什么?因为内部机房环境里快速跑通优先级更高,HTTPS证书走内部CA的签发流程比较慢。等后面统一规划时再切HTTPS也不迟。如果你要用自签HTTPS证书,注意所有要拉取镜像的客户端机器都要在Docker的daemon.json里把harbor.example.local加入insecure-registries,否则docker pull会报证书错误。
改完配置后,运行./install.sh,Harbor会启动nginx、core、registry、db、redis等一串容器。这一点和GitLab一样,Harbor本质上是docker-compose拉起一组服务,所以它的升级和迁移也是拷贝数据目录再重新install。
4.3 上传镜像与登录验证
Harbor跑起来后,第一步就是做登录验证:
docker login harbor.example.local:5080 -u admin然后给镜像打上带仓库地址的tag,推送:
docker tag nginx:alpine harbor.example.local:5080/library/nginx:alpine docker push harbor.example.local:5080/library/nginx:alpine如果推送失败,最常见的三个原因:网络不通、证书不受信任、账号密码没配。网络不通优先检查Harbor所在机器的防火墙端口;证书问题检查客户端的insecure-registries;账号密码问题确认密码有没有过期。
4.4 镜像加速与分发的小经验
Harbor本身还可以配置“Proxy Cache”,指向外部公共镜像仓库作为上游缓存。这样内网机器拉公共镜像时,Harbor能自动缓存一份到本地,第二次拉取就很快。这个功能对大集群来说非常实用,我建议开启。
另外,Harbor的镜像清理功能要谨慎开启。自动清理策略如果配置不当,可能把线上正在使用的镜像tag清掉。我这边没有开自动清理,都是人工定期清理“未使用且已过保留周期”的镜像,宁可多存一点,也不冒把镜像删没的风险。
5. 从提交代码到发布上线:Webhook触发与一条完整Pipeline的跑通
5.1 先设计链路,再写配置
组件都跑通后,剩下的核心工作就是把三者串起来。我习惯先把整体链路用文字画清楚,再动手配置:
开发提交代码到GitLab分支 → GitLab通过Webhook通知Jenkins → Jenkins触发Pipeline任务 → Pipeline从GitLab拉取代码 → 执行构建(编译、打包、跑测试) → docker build构建镜像 → docker push推送到Harbor → 生产服务器登录Harbor拉取镜像并重启容器。
5.2 凭证管理是打通的核心
环境搭建的难点通常不是功能配置,而是凭证。这里需要准备三类凭证:
- GitLab的Personal Access Token:Jenkins拉代码时用
- Harbor的用户名密码或机器人账号:Jenkins推送镜像时用
- 生产服务器的SSH密钥:Jenkins发布时远程执行命令用
统一放在Jenkins的Manage Credentials里,分别起好ID:gitlab-token、harbor-robot、deploy-ssh。Pipeline里直接引用这些ID,密钥本身不出现在代码里。
5.3 Pipeline脚本示例与解释
写一个最基础的Jenkinsfile,完整覆盖拉代码、构建镜像、推送镜像三步骤:
pipeline { agent any environment { HARBOR_ADDR = 'harbor.example.local:5080' IMAGE = "${HARBOR_ADDR}/library/myapp" PROJECT = 'devops/myapp' } stages { stage('Checkout') { steps { git branch: 'main', credentialsId: 'gitlab-token', url: 'http://gitlab.example.local:8929/devops/myapp.git' } } stage('Build and Test') { steps { sh 'mvn clean package -DskipTests=false' } } stage('Build Docker Image') { steps { sh 'docker build -t ${IMAGE}:${BUILD_NUMBER} .' } } stage('Push to Harbor') { steps { withCredentials([usernamePassword( credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PWD' )]) { sh ''' docker login ${HARBOR_ADDR} -u ${HARBOR_USER} -p ${HARBOR_PWD} docker push ${IMAGE}:${BUILD_NUMBER} ''' } } } } }构建镜像这一步有个细节:构建内容的上下文是WORKSPACE,所有依赖编译出来的产物都要在WORKSPACE里。有些项目把编译产物生成到容器外的目录,会导致docker build时找不到文件。
5.4 共同踩过的一个坑:登录GitLab时出现login failed
我这边在配置Jenkins的GitLab插件时,遇过一次“login failed. check api token or gitlab version”的报错。当时第一反应是token过期,重新生成token再试还是不行。后来发现是GitLab URL配置不对:Jenkins插件里填的是容器内部的http://gitlab:8929,而GitLab的external_url设的是外部地址。把插件里的GitLab Host URL改成外部访问地址,问题就消失了。这个报错很典型,遇到先检查地址,同时确认GitLab的版本和Jenkins插件的兼容性。
5.5 自动触发与手动触发并行
Webhook自动触发配置好之后,我特意保留了一个手动触发入口。原因是发布动作有时候需要人为控制节奏,比如周五下午的发布,自动触发可能直接把生产环境给更新了,这个风险是不该有的。我这边生产环境应用的流水线,默认不接Webhook,改为Jenkins界面上手动点击“Build Now”;测试环境的流水线才接Webhook,代码一提交就自动构建。
6. 跑起来之后的日子:资源规划、备份策略与生产环境踩过的坑
6.1 资源规划:先算内存,再看硬盘
这套系统跑起来半年多,最大的扩容压力不是CPU,是内存。GitLab、Jenkins、Harbor加在一起,一台4GB内存的机器根本扛不住。我给团队写的参考配置如下:
| 组件 | 建议内存 | 说明 |
|---|---|---|
| GitLab | 8GB | 内存不足直接表现为502,优先满足 |
| Jenkins | 4GB | 单Master加少量Agent够用 |
| Harbor | 4GB | 依赖多个子服务,联动占用多 |
| 宿主机总容量 | 16GB起步 | 给操作系统和Swap留余量 |
硬盘方面最需要关注的是Harbor的镜像存储目录和GitLab的仓库数据目录。我的经验是定期清理无用镜像、及时合并仓库分支,但硬盘容量本身宁可买大一些,镜像数据膨胀速度往往比想象中快。
6.2 备份策略三件套
容器化最大的好处就是备份简单,但前提是你知道该备什么。我的备份策略固定包含三块:
- GitLab备份:进入容器执行
gitlab-rake gitlab:backup:create,生成一个带时间戳的tar包,并把/var/opt/gitlab/backups目录同步到备份服务器 - Jenkins备份:直接归档
/var/jenkins_home目录,里面包含所有Job配置、插件状态和用户信息 - Harbor备份:备份数据库和data_volume,数据库可用docker exec进入db容器执行pg_dump导出Harbor库,data_volume直接同步
备份频率上我建议代码主仓数据每天一次,Jenkins配置每周一次,Harbor镜像每月一次。镜像本身也有恢复成本,真丢了可以直接从GitLab和代码重新构建,不必把Harbor备份做得太重。
6.3 Docker网络不通的排查思路
容器环境最常见的问题是“容器网络不通”。第一次遇到这个坑,我怀疑是docker0网桥的问题,后来发现是容器跨主机通信时端口没通。
排查网络问题我按这个顺序来:先确认容器是否正常启动,docker ps看看状态;再进入容器测试DNS解析和连通性;最后检查宿主机防火墙和云安全组规则。
最简单的连通性测试:
docker exec jenkins ping harbor如果是在同一个docker-compose网络内,服务名之间可以互相ping通。跨主机访问时,用实际IP地址测端口,比如telnet 192.168.1.10 5080。防火墙规则我这边吃过亏,Harbor对外服务的5080端口没有放行,导致Jenkins推送镜像一直超时,配置也检查了、账号也检查了,最后才发现是防火墙的问题。
6.4 Docker Desktop与本地验证环境的差异
在服务器上部署这套系统时,有不少团队会先在本地Docker Desktop上做验证。Windows和Mac上的Docker Desktop和Linux服务器上的Docker存在一些行为差异,比如虚拟机网络模式、文件挂载权限、性能表现等。
如果本地直接启动GitLab,Docker Desktop的内存默认只有2GB,GitLab启动很容易失败或502。解决办法是打开Docker Desktop设置,把Memory调到至少6GB。Windows上还会遇到hyper-v虚拟化未开启导致的“virtualization support not detected”提示,需要在BIOS里启用虚拟化功能。这些现象在服务器端一般不会出现,但确实会干扰刚开始实验的同事。
有一段时间我还看到有人在给Docker Desktop装汉化包,这个纯属于本地体验优化,不影响服务器端部署。真正该关注的是本地和服务器端镜像版本的漂移问题,两边尽量使用相同版本的docker引擎和相同版本的容器镜像。
6.5 日常运维中最实用的几个命令
系统跑起来之后,日常最多的运维动作就集中在几个命令上:
- 看容器状态:
docker ps -a - 看日志:
docker logs -f gitlab、docker logs -f jenkins - 进容器排查:
docker exec -it gitlab bash - 重启后验证:
docker-compose ps检查所有服务状态
我特别要提醒的是:不要想都不想就docker restart。GitLab这类应用重启可能引发数据恢复时间较长的问题,如果服务异常,先看日志再决定重启。Harbor的install.sh已经定义为幂等操作,重跑install脚本不会清掉数据,这倒是可以放心用的。
写在最后的运维心得
这套基于Docker容器、GitLab、Jenkins、Harbor组合的DevOps代码发布系统,运行到现在最直观的效果是:发版从全组人忙两小时,变成了线上点击一个按钮,五分钟之内完成构建、推送、发布全过程。开发再也不能说“我本地是好的”,因为构建环境是统一的容器环境;运维也不再担心“这个版本的镜像从哪来”,因为所有版本都在Harbor里留了痕迹。
最后分享一个我自己用的习惯:每次在Jenkins里新增一条发布流水线时,我都会顺手把Jenkinsfile提交到GitLab的项目仓库里,而不是在Jenkins界面上手工编辑保存。这样配置即代码,流水线的版本变化也有记录可查。等到团队规模起来之后,还可以把这些Jenkinsfile提成一个公共的Pipeline共享库,不同项目直接复用,连流水线本身都标准化起来。整个系统从代码提交到生产发布,每一环都可以查、可以回滚、可以追溯,这才是这套容器化DevOps方案真正的价值所在。