如果你和我一样,第一次在 Jenkins 里填 GitLab 仓库地址的时候卡了半个小时,那你应该能明白:拉代码这件事,看起来只是填一个 URL 和一个账号,真正跑起来之后,问题永远出在你没想到的地方。
这篇文章不打算讲太多的 CI/CD 大概念,就围绕一个非常具体的场景——Jenkins 从 GitLab 拉取代码——把这条链路上最容易出错、也最容易被教程几句话带过的环节拆开讲清楚。内容覆盖环境准备、凭据配置、Git 拉取失败的排查、任务创建以及从代码拉取到自动部署的完整过程。适合刚搭好 Jenkins 正准备接第一个构建任务的人,也适合已经在用、但经常被 “login failed” 和 “无法拉取代码” 折腾的运维和开发同学。
提示:全文以 Jenkins 2.x 版本为基础,文中涉及的插件名称和配置路径,在 Jenkins 2.3 及后续 LTS 版本中基本一致。
1. 动手之前,先想明白 Jenkins 和 GitLab 之间靠什么建立信任
很多人配置失败,不是因为操作不对,而是没搞懂底层逻辑。拉代码不是简单的“填个地址点保存”,Jenkins 要访问 GitLab,本质上要解决两件事:网络通不通,以及GitLab 认不认 Jenkins 这个客户端。前者靠网络配置,后者靠凭据。
1.1 一个被很多人忽略的前提:Jenkins 是“另一台电脑”
先建立一个关键认知:你在浏览器里打开 Jenkins 页面配置东西,但真正执行构建的是 Jenkins 节点,这个节点可能是服务器上一个 Java 进程,也可能是 Docker 容器里的一个 agent,它和你本地的电脑完全是两回事。
这意味着三件事:
- 你的电脑能访问 GitLab,不代表 Jenkins 服务器能访问。
- 你在本地生成的 SSH Key,不等于 Jenkins 服务器上有对应的私钥。
- 你本机配置的 Git 用户信息,和 Jenkins 节点没有任何关系。
所以每次搭建 Jenkins 拉代码之前,第一件事永远是去 Jenkins 所在的机器上自己先试一遍。这个习惯能省掉后面百分之八十的排错时间。
1.2 选对凭据方式:SSH 和 HTTP 的本质区别
从 GitLab 拉代码,协议层面只有两类选择:SSH和HTTP/HTTPS。很多教程会告诉你“两个都行”,但实际项目中这两个的坑完全不同。
HTTP 方式在 Jenkins 里的配置是:仓库地址填http://gitlab.example.com/group/project.git,凭据类型选 “Username with password”,里面填 GitLab 用户名和密码或 Access Token。这种方式的优点是配置直观,缺点是密码或 Token 容易过期,而且每次拉代码都要做一次认证,稍微有点慢。
SSH 方式则是:仓库地址填git@gitlab.example.com:group/project.git,凭据类型选 “SSH Username with private key”,里面填私钥。GitLab 通过公钥识别 Jenkins 的身份,不需要每次输入密码。这种方式更好用,而且在服务器上配置一次后,Git 命令行工具也能复用。
我的建议是:只要是服务器对服务器的场景,一律优先 SSH。原因不只是安全,更重要的是 SSH Key 不会像 Token 一样定期失效,省去的维护成本非常可观。
1.3 GitLab 侧的用户权限模型
GitLab 对仓库的访问权限分几个档位:Guest、Reporter、Developer、Maintainer、Owner。拉取代码至少需要Reporter权限,如果要做 Push 或合入分支操作,才需要更高的 Developer 及以上。
很多人在配置的时候图省事,随手建一个普通用户,然后发现构建报 403 或 401。这不一定是凭据格式写错了,很可能就是权限不够。如果你只是让 Jenkins 拉取代码构建,我建议直接在目标项目里创建一个Deploy Key(部署密钥),只授权这一个项目,权限只读,连额外账号都不用建。这种方式后面会详细讲。
还有一个容易忽略的点:GitLab 在不同版本里对用户名和仓库地址的解析方式有变化。比如老的 GitLab 支持用用户 ID 作为路径前缀,新版本则要求使用用户名或群组名。遇到 “Project not found” 时,先别急着怀疑网络,检查一下仓库路径是不是写成了机器 ID 或数字 ID。
2. 环境准备:把 Jenkins、GitLab 和网络这三件事一次理顺
在配置任何凭据之前,先把环境梳理清楚。我见过太多人,凭据配了半天,最后发现是 Jenkins 服务器少了git命令,或者 DNS 解析不到 GitLab 域名。
2.1 组件安装与版本选择
Jenkins 的安装方式有几种:直接下载 war 包用java -jar启动、用系统包管理器安装、用 Docker 启动。如果是生产环境,我比较推荐用 Docker 或者系统服务方式,方便跟随 LTS 版本升级。
这里有个版本敏感点:Jenkins 2.3 这个版本号本身比较老,但配置思路和现在的 LTS 版本没有本质区别。如果你是用 Docker 装,建议不要用latest标签,直接固定jenkins/jenkins:lts-jdk11或对应 JDK 版本的镜像,避免某天升级后插件不兼容。
GitLab 这边的安装不在本文重点,但如果你用 Docker 装 GitLab,注意端口映射和external_url配置。external_url直接决定 SSH 的 clone 地址格式,如果配的是http://192.168.1.10,那么 Jenkins 里 SSH 方式拉代码的地址就会带着 IP 而不是域名。
2.2 插件安装:Git Plugin、GitLab Plugin 和国内镜像源
Jenkins 拉取 Git 代码依赖两个基础插件:
- Git Plugin:提供 Git 操作核心能力,源码管理里能选 “Git” 全靠它。
- GitLab Plugin:提供 GitLab 连接、Merge Request 触发、Webhook 通知等能力。
如果你只打算实现“从 GitLab 拉代码到本地”,那么 Git Plugin 就够了。如果你想让 GitLab 的 Webhook 自动触发 Jenkins 构建,那 GitLab Plugin 也必须装。
这里要特别提一句插件安装源的问题。很多人在安装插件时卡死在 “该 Jenkins 实例似乎已离线”,其实不是真离线,而是默认插件源(updates.jenkins.io)访问不稳定。这时候去清华或华为的开源镜像站,把Update Site地址改成国内镜像,然后Manage Jenkins -> Manage Plugins -> Advanced里点一下 “Check now”,再回到插件列表就能正常下载了。
注意:换源只解决插件下载问题,不解决 GitLab 访问问题。这两个问题千万别混在一起排查。
举一个我实际遇到的例子:某次搭建 Jenkins,插件列表一直刷不出来,控制台提示连接 updates.jenkins.io 超时。我把站点地址换成https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json以后,插件瞬间就装上了。这类问题在服务器上非常常见,属于基础设施问题,不是 Jenkins 的 Bug。
2.3 用 git ls-remote 验证“能不能拉到代码”
配置好插件和环境变量后,别急着去 Jenkins 页面创建任务。先在 Jenkins 服务器上用命令行验证一遍,这一步能隔离掉大部分网络问题。
假设你已经在 GitLab 上创建了一个 SSH Key 对,并且把公钥放到了 GitLab 用户或 Deploy Key 里,那么在服务器上执行:
git ls-remote git@gitlab.example.com:group/project.git如果输出了一串 refs,说明 SSH 协议和网络都没问题。如果提示Permission denied (publickey),说明公钥没配好或者私钥路径不对。这样可以快速定位问题。
HTTP 方式的验证更简单:
git ls-remote http://gitlab.example.com/group/project.git按提示输入用户名和密码(或 Access Token),能输出 refs 就说明网络通。关键是这一步做完之后,要记住用的是哪个账号、哪个 Token,后面在 Jenkins 里填的就是这套。
3. 凭据配置:SSH Key 与 Access Token 的正确用法,以及 login failed 的完整排查
凭据是 Jenkins 拉取 GitLab 代码最核心也最容易出问题的一环。基本上所有“拉不到代码”的疑难杂症,最后都能归结到凭据上。
3.1 用 Deploy Key 完成只读拉取
先推荐一种相对来说最干净的方式:Deploy Key。
在 GitLab 项目页面进入Settings -> Repository -> Deploy keys,把 Jenkins 服务器上的公钥内容粘贴进去。这种方式的好处是:
- 不需要额外创建 GitLab 账号。
- 只对这一个项目生效,权限范围最小。
- 可以设置 “Write access allowed” 来决定是否允许推送,日常构建建议不要开启写权限。
Jenkins 服务器上生成 Key 的命令是:
ssh-keygen -t ed25519 -C "jenkins@build-server" -f ~/.ssh/jenkins_gitlab_ed25519然后把生成的jenkins_gitlab_ed25519.pub内容贴到 GitLab 的 Deploy Keys 里。Jenkins 侧配置时,凭据类型选 “SSH Username with private key”,Username 填git,Private key 直接粘贴私钥文件内容,或者选择 “From the Jenkins master ~/.ssh”。
有一点需要注意:如果你用的是 Docker 方式启动 Jenkins,Jenkins 进程运行在容器里,宿主机的~/.ssh对它是不可见的。这时候要么把私钥内容直接粘贴到凭据里,要么用 volume 把宿主机的.ssh目录挂载进容器。我通常选择前者,因为可维护性更好,换机器也不怕丢。
3.2 用 Access Token 连接 GitLab API
Deploy Key 解决的是 Git 协议层的认证问题。但如果你的 Jenkins 还需要调用 GitLab API(比如创建 Merge Request、读取项目列表、触发 Webhook),那还需要在 GitLab 里生成一个Personal Access Token或Project Access Token。
生成位置在 GitLab 右上角头像 ->Preferences -> Access Tokens(老版本在Settings -> Access Tokens)。生成时勾选api权限,有效期按需设置。
拿到 Token 后,在 Jenkins 的Manage Jenkins -> Configure System -> GitLab里配置连接信息:
- Connection name:随便填,比如
gitlab-prod - GitLab host URL:填 GitLab 的外网访问地址,例如
http://gitlab.example.com - Credentials:选 “GitLab API token”,粘贴 Token
填完之后点 “Test Connection”,正常情况下会显示Success。如果这里就报错,那就是 API Token 或网络问题,和后面任务里的 Git 凭据无关。
3.3 “login failed, check api token or gitlab version” 根因分析
这个报错非常经典,原文一般长这样:
login failed. check api token or gitlab version. log in via git if the version is supported.先说结论:这个错误出现在Test Connection或任务构建时 GitLab Plugin 调用 GitLab API 的阶段,和 “git 拉取代码”本身是两回事。它说明 Jenkins 的 GitLab 插件尝试用 API Token 访问 GitLab 接口,但失败了。
常见原因按概率排序:
- API Token 无效或已过期。重新生成一个 Token,注意勾选
api权限。 - 网络不通。在 Jenkins 服务器上用
curl手动访问 GitLab API 试试,例如:
如果返回curl -H "PRIVATE-TOKEN: 你的token" http://gitlab.example.com/api/v4/version{"version":"xx.x.x"...},说明网络正常,问题在插件侧。 - SSL 证书问题。如果 GitLab 是自签名证书,HTTP 访问没问题,但 Jenkins 用 HTTPS 时可能因为证书不受信任而失败。解决方式是让 Jenkins 信任该证书,或者把 GitLab 配置成 HTTP 内网访问。
- 插件版本与 GitLab 版本不兼容。老的 GitLab Plugin 调用的 API 接口在新版 GitLab 里被废弃,或反过来。升级插件或 GitLab 时,这个报错很容易突然出现。
排查的时候别一头扎进 Jenkins 设置里翻,要给问题分层:先确认 API 能通,再确认 Token 有效,最后才是插件兼容性。
3.4 从“手动测试”到“填入 Jenkins”的验证顺序
我自己的固定流程是这样的,分享给你作为参考:
- 在服务器上用
git ls-remote验证 Git 协议层通不通。 - 用
curl验证 API 通不通。 - 在 GitLab 侧确认账号权限和 Token Scope。
- 把凭据填入 Jenkins,在
Credentials管理页可以点 “Verify” 测试,但注意这里的验证不一定等于构建时的行为。 - 创建一个最简单的 “Freestyle project”,只做 “Git 拉取” 这一个动作,跑一次构建,看控制台输出。
我见过最多的翻车现场是:有人跳过了前两步,直接在 Jenkins 里配置,结果报错以后分不清到底是网络、凭据、还是权限问题。先手动验证,再填 Jenkins,这是唯一可靠的方法。
4. 创建构建任务:分支、仓库地址和触发方式的最佳实践
环境通了、凭据也配好了,接下来才是真正的 “从 GitLab 拉代码” 环节。这里的一些参数设置,直接影响构建速度和稳定性。
4.1 自由风格任务还是 Pipeline
实现拉代码这个动作,有两种常见方式:
自由风格任务(Freestyle project)的界面比较直观,适合初学者和简单的构建场景。在 “Source Code Management” 里选 Git,填入 Repository URL,选择凭据,指定 Branches to build,就完成了。优点是上手快,缺点是一旦构建步骤变多,难以维护。
Pipeline则是把整个构建过程写成Jenkinsfile,代码即配置,适合复杂流程。拉代码的 Pipeline 片段大概是这样的:
pipeline { agent any stages { stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: [[name: env.BRANCH_NAME]], extensions: [], userRemoteConfigs: [[ url: 'git@gitlab.example.com:group/project.git', credentialsId: 'jenkins-gitlab-ssh-key' ]] ]) } } } }如果你以后要上 “自动化部署”“多分支流水线”,建议直接学 Pipeline,省得以后迁移一次。
4.2 分支参数化与浅克隆
默认情况下,Jenkins 每次构建都会执行一次git fetch,把远端所有分支的引用都拉下来。仓库小还好,仓库一旦大(比如包含多年历史、大量二进制文件的仓库),构建时间和磁盘占用会非常难看。
两种优化手段很实用:
- 浅克隆(Shallow clone):在 Git Plugin 的 “Additional Behaviours” 里选择 “Advanced sub-modules behaviours” 或直接加 “Shallow clone” 行为,设置
depth为 1。这样只拉取最新一次提交,构建速度能快几倍。 - 指定分支:如果项目固定发布分支为
master或main,直接把 Branches to build 写成*/master,不要写成**。
如果你需要“构建时手动选择拉取哪个分支”,可以安装Git Parameter Plugin,在任务参数里加一个 Git 参数,类型选 “Branch”,这样每次构建前端会有一个下拉框,动态读取远端分支列表,选择后作为拉取分支传入。
这一步在很多团队里是刚需:开发要测某个特性分支,测试要部署稳定分支,运维要随时发布 hotfix。没有 Git Parameter 的话,就只能每次改配置里的分支名,或者为每个分支建一个任务,明显太低效。
4.3 触发方式:轮询与 Webhook 怎么选
让 Jenkins 知道“代码更新了”,有两种思路。
轮询(Poll SCM)是最简单的方式:设置一个 cron 表达式,比如H/5 * * * *,每 5 分钟检查一次远端仓库,有变化才触发构建。缺点是检查有延迟,而且每次都会向 GitLab 发请求,仓库多时会给 GitLab 带来压力。
Webhook是 GitLab 在代码提交或合并请求事件发生时,主动通知 Jenkins。在 GitLab 项目里进入Settings -> Webhooks,添加 Jenkins 的 Webhook 地址,例如:
http://jenkins.example.com/project/你的任务名但如果你的 Jenkins 在 NAT 后面或没有公网 IP,GitLab 无法主动访问,那就只能靠轮询。另外需要注意 Webhook 地址末尾的路径,和 Jenkins 插件版本有对应关系,老版本可能要加/git/notifyCommit,新版本直接用project/任务名即可。
我实际踩过的一个坑是:配了 Webhook 之后,GitLab 那边显示请求成功,但 Jenkins 迟迟不触发。最后发现是 Jenkins 的 “CSRF Protection” 阻止了匿名请求,需要在系统配置里把 GitLab 服务器的 IP 加入白名单,或者在 Webhook 请求里带上 API Token。这个问题很隐蔽,因为 Webhook 日志里看到的可能还是 200 状态码。
4.4 构建环境中的“隐藏变量”
拉取代码成功后,构建脚本经常需要知道自己现在在哪、这次构建是第几次。Jenkins 内置了很多环境变量,下面这几个在“拉代码 -> 构建”场景里最常被使用:
| 变量名 | 含义 | 典型用途 |
|---|---|---|
WORKSPACE | 工作目录,代码被拉取到的位置 | 构建脚本里切换到代码根目录 |
JOB_NAME | 当前任务名 | 日志、通知消息中标识任务 |
BUILD_NUMBER | 构建序号 | 产物版本号、镜像标签 |
GIT_COMMIT | 拉取到的 commit SHA | 记录构建对应的代码版本 |
GIT_BRANCH | 拉取的分支名 | 区分环境部署分支 |
建议别在生产构建脚本里写死/var/lib/jenkins/workspace/xxx这种路径,直接引用$WORKSPACE,这样任务迁移或者换节点不会出问题。
5. 把一段构建跑通:从拉取代码到 Java Web 应用自动部署
光拉代码不算完整的自动化,拉完之后能构建、能发布,这条链路才真正跑起来。这一步以一个典型的 Java Web 应用为例,给你一套可直接参考的思路。
5.1 一次标准的构建过程
代码已经通过 Git 插件拉取到WORKSPACE,接下来构建节点上需要准备 JDK 和 Maven 等工具。在自由风格任务的 “Build” 部分,添加一个 “Invoke top-level Maven targets”,填写:
Goals: clean package -DskipTests如果你用 Pipeline,对应的步骤是:
stage('Build') { steps { sh 'mvn clean package -DskipTests' } }这里有一个非常容易踩的坑:Jenkins 节点上装的是哪个版本的 JDK,和项目要求的是否一致。很多项目用 Java 8 编译,但构建节点默认 JDK 是 17,结果编译报错。解决方式是安装JDK 参数插件或者在全局工具配置里定义多个 JDK,让不同的任务选不同的版本。
在配置 “Invoke top-level Maven targets” 时,如果下拉列表里没有 Maven,需要在Manage Jenkins -> Global Tool Configuration里先添加 Maven 安装,可以自动下载,也可以指向本机已经装好的 Maven。
5.2 远程发布与部署结果验证
构建产物是target/xxx.war或target/xxx.jar,下一步是把产物发布到目标服务器。
常见的做法是使用Publish Over SSH插件:在系统配置里填好一个 SSH Server,然后在任务 “Post-build Actions” 里添加 “Send build artifacts over SSH”,设置:
- Source files:
target/*.war - Remove prefix:
target - Remote directory:
/opt/app - Exec command:执行重启脚本,比如
systemctl restart tomcat或docker compose up -d
这里有个细节:确认 Jenkins 所在机器能通过 SSH 免密登录目标服务器,如果不行,在 Publish Over SSH 的配置里填好用户名和私钥。目标服务器的目录权限也要提前确认,否则文件传过去却没权限写,日志里只会显示 “Permission denied”。
部署完成后的验证也很重要。发布动作本身只是把文件丢过去,应用是否正常起来是另一回事。建议在 Exec command 里加一段健康检查:
curl -sf http://127.0.0.1:8080/health || exit 1这样 Jenkins 会认为重启失败,构建标红,不会出现“构建成功但应用挂了”的假象。
5.3 复用与扩展:把 Pipeline 沉淀成模板
当你有多个项目都要走 “拉代码 -> 构建 -> 部署” 的流程时,重复配置自由风格任务会很痛苦。更合理的做法是把构建逻辑写进项目的Jenkinsfile,在代码仓库里维护。
pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '10')) disableConcurrentBuilds() } environment { APP_NAME = 'my-web-app' REGISTRY = 'registry.example.com' } stages { stage('Checkout') { steps { checkout scm } } stage('Build') { steps { sh 'mvn clean package -DskipTests' } } stage('Archive') { steps { archiveArtifacts artifacts: 'target/*.war' } } stage('Deploy') { when { branch 'main' } steps { sshPublisher( publishers: [ sshPublisherDesc( configName: 'prod-server', transfers: [ sshTransfer( sourceFiles: 'target/*.war', remoteDirectory: '/opt/app', execCommand: 'sh /opt/app/deploy.sh' ) ] ) ] ) } } } }这里checkout scm会自动使用任务里配置的仓库地址和凭据,不需要重复写。按分支判断是否部署,是通过when { branch 'main' }控制的,这样同一个 Pipeline 既能测试环境发布特性分支,也能在生产环境只发布主干,逻辑清晰,也不容易改错。
5.4 顺手解决的常见构建问题
最后列几个我在这个过程中真实遇到过、且搜索引擎里经常被搜到的问题:
问题一:git 拉取代码的时候提示 未能顺利退出(退出码 1)
这个报错本质是 Git 命令执行非零退出码。先看控制台日志中 Git 命令是哪个环节失败,重点检查:
- 凭据是否正确(SSH Key 还是 Token)。
- 分支名是否存在。
- 是否把
master写成了main。 - 如果是大仓库 clone 超时,在全局 Git 配置里把
http.postBuffer调大,或者直接用 SSH。
问题二:Jenkins 提示找不到 Git 仓库或项目
确认仓库地址写的是“可通过 Jenkins 访问的地址”,而不是你本地浏览器的地址。很多人在办公电脑上测试用的是 NAT 映射的域名,Jenkins 服务器解析不了,就会报 404 或 Host key verification failed。
问题三:GitLab CI/CD 里 Docker 镜像构建时报 daemon 错误
如果你用 GitLab Runner 构建 Docker 镜像,提示error response from daemon: Get ...,一般是 Runner 容器没有挂载 Docker socket,或者 registry 地址网络不通。这在“自动部署”场景下很常见,和 Jenkins 拉代码是两条链路,别混在一起排查。
最后聊两句我的实际体会
如果你问我做 Jenkins 拉代码这件事,最值得记住的一句话是什么,我会说:把“手动验证”变成肌肉记忆。所有配置问题的排查,都从 Jenkins 服务器上用命令行先试一次,而不是在网页上反复保存测试。
另一个经验是,凭据尽量走到“少而稳”:能用 SSH Key 就不用密码,能用 Deploy Key 就不额外建账号,能用一个长期有效的 Token 就不要再造第二个。这样即使团队有人离职或者 GitLab 升级,也不至于让构建一夜之间全红。
Jenkins 拉 GitLab 代码只是整个交付链路的起点,但它也是最基础的地基。希望这篇文章能帮你把这一步走稳,后面无论是做多分支流水线、镜像构建还是自动部署,都会顺手很多。