Ubuntu 22.04 搭建 GitLab + Jenkins 自动化部署流水线实践

发布时间:2026/10/12 20:44:32
Ubuntu 22.04 搭建 GitLab + Jenkins 自动化部署流水线实践
前一段工作告一段落正好把最近在 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 核 4G50G 磁盘代码仓库、Webhook 事件源Jenkins 服务器2 核 4G50G 磁盘执行构建、测试、部署任务部署目标服务器按业务需求运行 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-serverGitLab 仓库操作和 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_URLhttp://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 都支持得不错所以先装 JDKsudo 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 jenkinsJenkins 默认跑在 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 对应 8081Jenkins 对应 8080server { 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 串起来项目、令牌、Webhook3.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-prodGitLab 地址http://git.example.comCredentials选择之前创建的 GitLab 令牌凭据类型选“GitLab API token”接着创建两个常用凭据Git 仓库访问凭据类型选“Username with password”使用jenkins-bot用户和密码或者用该用户的访问令牌也可以。部署服务器 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 项目里实际使用的精简版 Jenkinsfilepipeline { 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外层加parametersparameters { 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-backendSSH 免密要以 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 deploy192.0.2.10把公钥拷到部署机器的deploy用户后Jenkins 里的 SSH 私钥凭据对应填/var/lib/jenkins/.ssh/id_ed25519的内容。注意不要在 Jenkins 的 ssh 命令里简单粗暴地-o StrictHostKeyCheckingno。正确做法是把部署机器的 host key 提前写入/var/lib/jenkins/.ssh/known_hosts既能避免首次连接卡在指纹确认又不至于完全放下主机校验。5.2 systemd 服务单元和部署后的启动方式部署脚本里常见到用nohup java -jar xxx 把服务拉起来这个做法问题很多SSH 会话断开、父进程退出、环境变量加载不完整都会导致服务异常。最稳的做法是交给 systemd 托管。在部署机上创建服务文件[Unit] Descriptiondemo-backend service Afternetwork.target [Service] Userdeploy Groupdeploy WorkingDirectory/opt/demo-backend EnvironmentFile/etc/demo-backend.env ExecStart/usr/bin/java -jar /opt/demo-backend/current.jar Restarton-failure RestartSec5 [Install] WantedBymulti-user.target把服务注册好sudo systemctl daemon-reload sudo systemctl enable demo-backendcurrent.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 一旦被拿到执行权限等于掌握了整条部署链路。优先做三件事启用 HTTPS。域名如果已经解析到公网 IP使用 Let’s Encrypt 之类的证书签发工具用 Certbot 配合 Nginx 模式签证书能避免 GitLab 和 Jenkins 的管理页面明文传输密码和令牌。给 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;关闭 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 的连通性和部署权限问题上。只要把权限和事件流理清楚剩下的无非是命令行的机械组合。真正有价值的部分不是某一条命令而是“代码推送之后经过一系列可重复的步骤稳定地把应用发布到目标环境”这件事本身。过程中遇到的各种报错大部分都能从日志和时间线里找到答案希望这篇记录能让你少走点弯路。