☰
Ubuntu 22.04 搭建 GitLab + Jenkins 自动化部署流水线实践
2026/10/12 20:44:23 网站建设 项目流程

前一段工作告一段落,正好把最近在 Ubuntu 22.04 上落地 GitLab + Jenkins 这套 CI/CD 的完整过程整理成笔记。起因也很朴素:团队每次发布版本都要有人远程登录测试机,手动拉代码、打包、传文件、重启服务,哪一步漏了都很难第一时间发现。某次因为只更新了 jar 包却没同步配置文件,问题直到用户反馈才暴露出来。当时就想,与其每次靠“提醒”来兜底,不如把流程固化成一条可以重复执行的流水线。

这篇记录围绕“在 Ubuntu 22.04 上安装 GitLab 和 Jenkins、打通推送触发构建、实现自动打包部署”展开,适合正准备搭建小型 CI/CD 环境的个人开发者或两三人的业务团队参考。我不会只贴命令,会尽量把每个配置背后的原因、踩过的坑、以及部署阶段容易忽略的权限和环境变量问题都讲清楚。整个方案不需要 Kubernetes,不依赖云厂商的托管 CI 服务,两台 2 核 4G 的 Ubuntu 主机就能跑起来,GitLab、Jenkins、Nginx 全部通过 apt 安装,维护成本相对可控。

1. 这一套组合到底解决什么问题

1.1 手动发版流程里最痛的那几件事

如果你经历过手动发版,应该对下面这个流程不陌生:提交代码、登录跳板机、拉取分支、执行构建、找到目标产物的路径、上传到服务器、备份旧包、重启服务、再盯一会儿日志。这套流程单次执行可能只要十分钟,但它的问题在于“人”是流程中最不稳定的环节。

  • 今天在 A 机器上构建,明天在 B 机器上构建,产物的环境和路径不一致。
  • 某次漏了备份,回滚时找不到上一个可用版本。
  • 测试环境的手工操作没有记录,线上出问题时无法追溯当时到底部署了什么。
  • 新成员加入后,很难从文字文档里完整复现“老手”的发布步骤。

CI/CD 解决的就是这些问题:把代码提交、静态检查、单元测试、打包、部署这些动作串成一条自动化链路,每次推送都会触发,构建过程有日志,失败时有通知,部署版本有记录。

1.2 GitLab 和 Jenkins 在流水线里的分工

很多人容易把 GitLab 和 Jenkins 混在一起,觉得都是“做 DevOps 的工具”。实际拆开看,两件事的分工非常清晰:

GitLab 负责代码托管、分支管理、合并请求、Webhook 事件等代码协作层面的工作。当开发者把代码推送到 GitLab,或者一个合并请求被接受时,GitLab 并不知道如何构建这个项目,但它能以 Hook 的方式把“有代码更新了”这个事件发出去。

Jenkins 负责拿到事件后真正做构建、测试、部署。它会根据配置去拉取代码,执行 Maven、Node、shell 等命令,把产物放到指定位置,再通过 SSH 推到目标服务器完成部署。

简单类比:GitLab 像前台接待,记录谁什么时候提交了什么;Jenkins 像执行部门,接到通知后开始干活,并把结果反馈给前台。

1.3 我的环境假设和最低要求

这次搭建使用的是两台 Ubuntu 22.04 服务器:

角色建议配置用途
GitLab 服务器2 核 4G,50G 磁盘代码仓库、Webhook 事件源
Jenkins 服务器2 核 4G,50G 磁盘执行构建、测试、部署任务
部署目标服务器按业务需求运行 systemd 服务的业务机器

如果资源紧张,把 GitLab 和 Jenkins 装在同一台机器上也行,但需要注意端口冲突,后面我会讲怎么处理。示例项目用demo-backend,一个 Java + Maven 项目,构建产物是 jar 包,最终通过 systemd 部署到目标服务器。换成 Node 项目或者 Python 项目也没关系,核心逻辑不变,只是构建命令不同。

2. Ubuntu 22.04 上把两台机器装到顺手为止

2.1 装软件之前的系统准备

很多人拿到新机器就直接开始装 GitLab,结果后面因为时区不对、openssh-server 没装、时区时间同步有问题,白白多折腾好几个小时。我习惯先做一轮基础准备:

sudo apt update && sudo apt upgrade -y sudo timedatectl set-timezone Asia/Shanghai sudo apt install -y curl openssh-server ca-certificates tzdata

这里关键是openssh-server,GitLab 仓库操作和 Jenkins SSH 部署都需要它。时区设置则避免后续日志时间看起来“慢了八小时”的错觉。

如果打算用 Nginx 做反向代理,也顺手装掉:

sudo apt install -y nginx

提示:执行某些安装脚本前,建议先打开脚本地址用浏览器看一眼内容,确认仓库地址和安装逻辑正常再执行,别把“curl 管道到 bash”变成无脑习惯。

2.2 GitLab CE 的安装和首次初始化

GitLab 分商业版和社区版,通常我们直接用社区版就够。安装过程并不复杂,但 reconfigure 阶段比较耗时,属于“卡住不要慌”的类型:

curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash sudo EXTERNAL_URL="http://git.example.com" apt install gitlab-ce

注意看EXTERNAL_URL,这个变量特别重要。我见过有人图省事直接用 IP 安装,后续如果换服务器或做反向代理,GitLab 内部生成的链接全是 IP,改起来非常难受。尽量在安装前就决定好域名,比如git.example.com。

安装完成后,GitLab 会自动执行 reconfigure,这个过程经常持续几分钟。完成后需要在浏览器里打开http://git.example.com并用 root 账号登录。首次登录密码在系统里有一个临时生成的文件:

sudo cat /etc/gitlab/initial_root_password

这个文件会在 24 小时内被自动删除,所以拿到密码后立刻登录并修改,是最关键的一步。

2.3 Jenkins 的安装和首次初始化

Jenkins 在 Ubuntu 22.04 上建议用官方 apt 源安装。Ubuntu 22.04 自带的 OpenJDK 版本比较新,官方 LTS 版本对 Java 11 / 17 都支持得不错,所以先装 JDK:

sudo apt install -y openjdk-17-jdk

然后添加 Jenkins 官方源并安装:

curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null sudo apt update sudo apt install -y jenkins sudo systemctl enable --now jenkins

Jenkins 默认跑在 8080 端口,使用独立的jenkins系统用户,工作目录在/var/lib/jenkins。打开页面后要求输入初始管理员密码:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

这一步建议选择“安装推荐的插件”,后面再按需要补装。推荐插件已经包含 Git、Pipeline 等常用能力,能省不少事。

2.4 用 Nginx 把 GitLab 和 Jenkins 的反向代理理顺

如果 GitLab 和 Jenkins 装在同一台机器上,最大的问题就是 80 端口冲突。GitLab 默认自带 nginx 监听 80,而 Jenkins 默认独占 8080。我采用的方案是:把 GitLab 的内部端口改为 8081,由统一的外部 Nginx 同时代理git.example.com和ci.example.com。

编辑 GitLab 配置:

sudo vim /etc/gitlab/gitlab.rb

找到相关行修改或追加:

external_url 'http://git.example.com' nginx['listen_port'] = 8081 nginx['listen_https'] = false

然后重新配置生效:

sudo gitlab-ctl reconfigure

外部 Nginx 中添加两个 server 块,GitLab 对应 8081,Jenkins 对应 8080:

server { listen 80; server_name git.example.com; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 100m; } } server { listen 80; server_name ci.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 100m; } }

这里要特别注意:GitLab 默认会对请求头里的 Host 做校验,外部反代必须把Host原样传过去,否则会出现“页面能打开但很多资源地址跳错”的问题。

防火墙方面,我只开放必须的端口:

sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable

不要把 8080 和 8081 直接暴露到公网,统一让外部通过 Nginx 的 80/443 进来,后续做 HTTPS 和访问认证也方便。

3. 把 GitLab 和 Jenkins 串起来:项目、令牌、Webhook

3.1 在 GitLab 侧准备好项目和个人访问令牌

两边的软件跑起来只是第一步,真正让它们联动需要三样东西:GitLab 项目、Jenkins 的 API 凭据、Webhook 配置。

先在 GitLab 创建一个用于测试的组和项目,例如cicd-demo/demo-backend。然后在 Jenkins 中访问 GitLab 时,最好使用一个独立的机器人用户,而不是 root 本身的令牌。

我踩过的坑:用 root 的 Personal Access Token 一时方便,但后期如果离职或误操作,登录权限很难精细化回收。单独建一个名为jenkins-bot的普通用户,只授予项目组开发者或报告者权限,再创建令牌,安全性和可维护性都更好。

创建令牌时权限范围至少要包含api,这样 Jenkins 插件才能读取项目信息、创建 Webhook 和触发构建。令牌生成后只会显示一次,复制保存好。

3.2 Jenkins 侧安装插件并配置系统级凭据

进入 Jenkins “系统管理 → 插件管理”,除了默认推荐的插件,我通常额外确认这几个:

插件用途
GitLab与 GitLab 集成,解析 Webhook 事件
Pipeline声明式流水线支持
SSH Agent在流水线中使用 SSH 私钥凭据
Credentials Binding把秘密参数注入构建环境
Timestamper构建日志显示耗时,排障好用

然后在“系统配置”里添加 GitLab 连接:

  • 连接名称:随意,例如git-lab-prod
  • GitLab 地址:http://git.example.com
  • Credentials:选择之前创建的 GitLab 令牌,凭据类型选“GitLab API token”

接着创建两个常用凭据:

  1. Git 仓库访问凭据,类型选“Username with password”,使用jenkins-bot用户和密码,或者用该用户的访问令牌也可以。
  2. 部署服务器 SSH 私钥,类型选“SSH Username with private key”,用户名写目标服务器上的部署用户(后面建),私钥粘贴 Jenkins 所生成的私钥内容。

创建凭据的路径是“系统管理 → 凭据 → 系统 → 全局凭据 → 添加凭据”。这里的credentialsId(比如我下面写的deploy-key)会在 Jenkinsfile 里被引用,命名时注意结构清晰。

3.3 Webhook 配置和常见联动失败原因

在 Jenkins 里新建一个 Pipeline Job,源码管理选择 Git,填上 GitLab 仓库地址和凭据,然后勾选“Build when a change is pushed to GitLab”。保存后,Job 页面会生成一个 Webhook URL 和 Secret Token。

把这个 URL 和 Token 填到 GitLab 侧:项目 → 设置 → 网络钩子。触发事件至少勾选“推送事件”,如果希望合并请求也能触发验证,可以一并勾选“合并请求事件”。

Jenkins Webhook 示例 http://ci.example.com/project/demo-backend Secret Token 示例 gJlQk2x0..."

配置完成后,在 GitLab 的 Webhook 页面点“测试 → Push events”。返回 200 表示打通。如果联动失败,重点检查四个方向:

现象常见原因
测试 Webhook 返回 304 或 404Webhook URL 写错,或 Jenkins Job 名称包含空格/特殊字符,需要 URL 编码
点击测试能通但实际推送不触发项目 hooks 权限不足,jenkins-bot对项目没有足够权限
Jenkins 收到事件但没构建系统配置中的 GitLab 连接没配好,或条件分支过滤设置不对
一直返回 504外网或同网段访问不到 Jenkins,检查防火墙和 Nginx 配置

这块最让人困惑的点是“测试能通但实际不触发”,很多时候不是配置问题,而是 GitLab 的机器人用户权限没放开,事件发不出来或发送不完整。先给jenkins-bot在项目里授受适当的 Reporter 或 Developer 权限,再逐一排查。

4. 第一个流水线:从 Jenkinsfile 骨架开始

4.1 为什么直接上 Pipeline Job 而不是 Freestyle Job

早期很多教程用 Freestyle Job 通过界面点选构建步骤,适合快速验证,但问题也很明显:配置存在 Jenkins 里,不属于代码领域,换台 Jenkins 就要重新点一遍。推荐直接使用 Pipeline Job,把Jenkinsfile放进项目仓库,流水线本身就是版本化代码,新人看文件就能理解构建过程。

声明式流水线(Declarative Pipeline)语法更结构化,读起来像“配置 + 步骤”,比脚本式流水线更克制,也更加适合作团队基线。

4.2 一个可运行的 Java 项目 Jenkinsfile

下面是我在一个 Maven 项目里实际使用的精简版 Jenkinsfile:

pipeline { agent any options { timestamps() disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5')) } tools { jdk 'JDK17' } environment { DEPLOY_HOST = '192.0.2.10' DEPLOY_USER = 'deploy' APP_NAME = 'demo-backend' } stages { stage('Checkout') { steps { checkout scm } } stage('Unit Test') { steps { sh './mvnw test' } } stage('Build') { steps { sh './mvnw -q clean package -DskipTests' } } stage('Deploy') { when { branch 'main' } steps { sshagent(['deploy-key']) { sh """ set -e ssh ${DEPLOY_USER}@${DEPLOY_HOST} "mkdir -p /opt/${APP_NAME}/releases" scp target/${APP_NAME}.jar ${DEPLOY_USER}@${DEPLOY_HOST}:/opt/${APP_NAME}/releases/${APP_NAME}-${BUILD_NUMBER}.jar ssh ${DEPLOY_USER}@${DEPLOY_HOST} "ln -sf /opt/${APP_NAME}/releases/${APP_NAME}-${BUILD_NUMBER}.jar /opt/${APP_NAME}/current.jar && sudo systemctl restart ${APP_NAME}" """ } } } } post { success { echo "构建成功: ${env.BUILD_URL}" } failure { echo "构建失败: ${env.BUILD_URL}" } } }

几个容易被忽略的细节:

  • checkout scm会自动按 Pipeline Job 源码管理里配置的仓库地址和分支拉代码,不用手动写git clone。
  • tools块引用的JDK17需要在“系统管理 → 全局工具配置”里预定义,安装方式和路径随意。
  • 使用./mvnw而非全局mvn,项目里的 Maven Wrapper 能锁定版本,构建环境更一致。
  • sshagent(['deploy-key'])会在当前构建节点临时加载 SSH 私钥,环境变量方式不可用时,这个插件是处理私钥最干净的手段。
  • scp和ssh命令里使用${BUILD_NUMBER}是为了区分每次构建产生的不同产物,方便回滚。

4.3 参数、手动审批和不同分支的差异化处理

流水线一旦跑起来,需求会很快变多,最常见的有三种:

手动参数化部署

在pipeline外层加parameters:

parameters { string(name: 'REMOTE_TAG', defaultValue: 'latest', description: '需要部署的镜像或版本标签') }

然后在部署阶段使用${params.REMOTE_TAG},这样既能实现“一键部署”,又能做到发版前确认版本号。

带审批的发布

对生产环境,不要直接自动部署,加一层input让操作人确认,可以减少误触发:

stage('Confirm Production Deploy') { when { branch 'main' } steps { input message: '确认部署到生产环境?', ok: '开始部署' } }

按分支执行不同阶段

测试环境通常每个分支都构建,生产环境只接受main分支或特定 Tag。用when { branch 'main' }或when { tag pattern "v.*" }控制即可,不要让一个 Job 把所有环境全写死。

5. 部署环节最容易翻车的三个细节

5.1 专用部署用户和 SSH 免密的正确姿势

很多运维习惯用 root 部署,这样确实省事,但风险也太大了。Jenkins 里一旦凭据泄露,攻击者等于拿到了服务器的 root 权限。建议单独创建一个低权限用户:

sudo adduser deploy sudo mkdir -p /opt/demo-backend/releases sudo chown -R deploy:deploy /opt/demo-backend

SSH 免密要以 Jenkins 节点的jenkins用户身份建立,不是用你自己登录服务器的账号。在 Jenkins 服务器上执行:

sudo -H -u jenkins bash -c "ssh-keygen -t ed25519 -N '' -f /var/lib/jenkins/.ssh/id_ed25519" sudo -H -u jenkins ssh-copy-id deploy@192.0.2.10

把公钥拷到部署机器的deploy用户后,Jenkins 里的 SSH 私钥凭据对应填/var/lib/jenkins/.ssh/id_ed25519的内容。

注意:不要在 Jenkins 的 ssh 命令里简单粗暴地-o StrictHostKeyChecking=no。正确做法是把部署机器的 host key 提前写入/var/lib/jenkins/.ssh/known_hosts,既能避免首次连接卡在指纹确认,又不至于完全放下主机校验。

5.2 systemd 服务单元和部署后的启动方式

部署脚本里常见到用nohup java -jar xxx &把服务拉起来,这个做法问题很多:SSH 会话断开、父进程退出、环境变量加载不完整,都会导致服务异常。最稳的做法是交给 systemd 托管。

在部署机上创建服务文件:

[Unit] Description=demo-backend service After=network.target [Service] User=deploy Group=deploy WorkingDirectory=/opt/demo-backend EnvironmentFile=/etc/demo-backend.env ExecStart=/usr/bin/java -jar /opt/demo-backend/current.jar Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

把服务注册好:

sudo systemctl daemon-reload sudo systemctl enable demo-backend

current.jar是一个指向具体发布版本的符号链接,每次部署时用ln -sf切换到新版本。这样回滚时只需要把符号链接重新指回上一版 jar,再重启服务,不需要把历史包物理挪动位置。

如果希望deploy用户能重启服务而不需要密码,可以配置一条精细化的 sudo 规则:

echo 'deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart demo-backend' | sudo tee /etc/sudoers.d/demo-backend

这个做法比直接给deploy用户开放所有 sudo 权限要安全得多,而且 Jenkins 部署阶段里那句sudo systemctl restart demo-backend就能顺利执行。

5.3 权限、环境变量、旧包堆积这些不起眼的问题

部署阶段出现的故障,很大比例不是代码问题,而是服务器环境问题。

权限问题。目标目录/opt/demo-backend必须属于deploy用户,否则scp会直接失败,错误信息可能只在日志中表现为 “Permission denied”。这种错误排查时先ls -ld看清目录属主,别急着改脚本。

环境变量问题。systemd 服务不会加载 shell 的.bashrc/.bash_profile,数据库地址、密钥这类配置不要塞在启动命令里,统一放在/etc/demo-backend.env,文件权限设为 600,然后让EnvironmentFile指向它。

sudo chown root:deploy /etc/demo-backend.env sudo chmod 640 /etc/demo-backend.env

旧包堆积问题。每次部署生成一个带构建号的 jar,长期积累会吃满磁盘。可以在系统层面定期清理,只保留最近几个版本:

sudo find /opt/demo-backend/releases -name '*.jar' -mtime +30 -delete

也可以直接在 Jenkinsfile 里把产物保留数量交给buildDiscarder管理,释放 Jenkins 侧的构建记录和产物,降低连续构建期间磁盘膨胀的风险。

6. 上线之后的安全加固与日常维护建议

6.1 别让 Jenkins 和 GitLab 裸奔在浏览器里

流水线跑通之后的下一步,不是继续加功能,而是把安全补齐。我见过不少团队把 Jenkins 的 8080 端口直接暴露在公网,管理页面没有任何访问控制,这是非常危险的,因为 Jenkins 一旦被拿到执行权限,等于掌握了整条部署链路。

优先做三件事:

  1. 启用 HTTPS。域名如果已经解析到公网 IP,使用 Let’s Encrypt 之类的证书签发工具,用 Certbot 配合 Nginx 模式签证书,能避免 GitLab 和 Jenkins 的管理页面明文传输密码和令牌。
  2. 给 Jenkins 的 Nginx 站点加一层 Basic Auth。在 Nginx server 块里加入认证配置,即使公网能访问到ci.example.com,没有账号也进不了管理界面:
sudo apt install -y apache2-utils sudo htpasswd -c /etc/nginx/.htpasswd ci_admin

然后在 Jenkins 的 Nginx 配置 location 块中加上:

auth_basic "CI Access"; auth_basic_user_file /etc/nginx/.htpasswd;
  1. 关闭 GitLab 公开注册。如果不需要对外开放注册,管理员在管理后台将“允许新用户注册”关闭,避免陌生账号扫描进来。

还有一个容易被忽略的点:GitLab 初始root密码要立即修改,Jenkins 初始管理员密码只要用一次,授权完成后建议停用或改写初始账号策略。凭据令牌一旦发现外流,马上到 GitLab 用户设置里吊销重建。

6.2 构建缓存与流水线提速

流水线稳定后,构建速度会逐渐成为排队的瓶颈。Maven 项目最有效的提速手段是“保留依赖缓存”。Jenkins 节点上的~/.m2/repository第一次构建会慢慢下载依赖,第二次之后命中本地仓库,速度会有量级提升。

不要每次构建前都强行删除工作区。默认情况下 Jenkins 会在同一个 workspace 上复用,只要保证代码用 Git 流转,本地目录的缓存可以大胆保留。如果担心磁盘被多个项目占满,可以给 Jenkins 节点配置独立的构建磁盘,或者定期通过 Groovy 清理长时间不用的 workspace。

还有两个常见调整:

  • 把静态检查、单元测试、构建产物校验这几个阶段尽量并行执行,利用 Jenkins 的parallel块,让 CPU 多核充分跑起来。
  • Jenkins 的构建日志输出加上timestamps(),方便看每一步耗时,定位“卡在哪里”比“报什么错”更重要。

6.3 磁盘日志和备份策略

CI 系统本身也是业务系统,不备份等于白搭。我的习惯是:

  • GitLab 每天凌晨做一次全量备份。GitLab 自带gitlab-backup create命令,配合 crontab 定时执行,把备份文件同步到另一台机器或对象存储。
  • Jenkins 不常用自带备份插件,直接把/var/lib/jenkins里关键的jobs、credentials、plugins目录做 tar 归档即可。配置和 Jenkinsfile 都属于“可以重建的代码”,真正不可再生的是凭据和 Job 配置。
  • 对构建记录设置buildDiscarder策略,保留最近 30 次或最近 10 个版本,防止构建历史和日志文件无限膨胀。

实际操作中我还有一个习惯:每次升级插件或改动系统级配置前,先快照或打包/var/lib/jenkins,一旦升级后出现插件不兼容,可以快速回退。这个习惯落地的成本很低,却能在关键时刻省下半天恢复时间。

这套流程从安装到跑通,前后大概花了两天,其中大部分时间花在 Webhook 的连通性和部署权限问题上。只要把权限和事件流理清楚,剩下的无非是命令行的机械组合。真正有价值的部分不是某一条命令,而是“代码推送之后,经过一系列可重复的步骤,稳定地把应用发布到目标环境”这件事本身。过程中遇到的各种报错,大部分都能从日志和时间线里找到答案,希望这篇记录能让你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询