GitLab CI/CD 使用指南(小白版)
零、先搞懂:CI/CD 到底是什么?
用快递公司来类比 🏭
想象你开了一家网店,每次有订单,你的流程是:
客户下单 → 仓库打包 → 质检 → 快递发货
但如果你每来一单都自己打包、自己检查、自己发快递,那效率极低。
CI/CD 就是雇佣一个机器人帮你自动干这些事:
| 概念 | 快递类比 | 实际含义 |
|---|---|---|
| CI(持续集成) | 机器人自动打包+质检 | 代码提交后,自动编译、自动测试,确保代码没问题 |
| CD(持续部署) | 机器人自动发货给客户 | 测试通过后,自动把程序部署到服务器上,用户就能用了 |
一句话:你只管写代码、推送到 GitLab,剩下的编译、测试、部署全部自动完成。
一、五个核心概念(用流水线工厂来理解)
想象一个汽车制造流水线:
┌──────────────────────────────────────┐
代码推送 ──→ │ Pipeline(整条流水线) │
│ │
│ Stage 1: 冲压车间(必须完成) │
│ ├── Job: 冲压车门 ← 同时干 │
│ └── Job: 冲压车顶 ← 同时干 │
│ ↓ │
│ Stage 2: 焊接车间(冲压完成才能开始) │
│ └── Job: 焊接车身 │
│ ↓ │
│ Stage 3: 喷漆车间 │
│ └── Job: 喷漆 │
└──────────────────────────────────────┘
| 概念 | 汽车工厂类比 | 大白话解释 |
|---|---|---|
| Pipeline(流水线) | 整条生产线 | 一次完整的"代码→部署"过程,由 .gitlab-ci.yml 文件定义 |
| Stage(阶段) | 冲压→焊接→喷漆 | 大的步骤分组,必须按顺序执行,前一个失败后面就不做了 |
| Job(作业) | 焊接车身这个具体工作 | 一个具体的任务,同一 Stage 里的多个 Job 各干各的同时进行 |
| Runner(执行者) | 厂房里的机器人 | 一台真实的机器(你的服务器),负责真正动手干活 |
| Artifacts(制品) | 造好的半成品车门 | 一个 Job 做出来的东西,传给下一个 Job 继续用 |
这些概念在文件里长什么样?
来看看你项目中 .gitlab-ci.yml 的对应关系:
stages: # ← 定义有哪些车间(Stage)
- test # 只有一个:test 车间
hello-job: # ← 这是一个 Job(作业),名字随便取
tags: # ← 指派给哪个机器人(Runner)干
- ubuntu-24.04
stage: test # ← 这个 Job 在 test 车间
script: # ← 要干的活儿:跑这些命令
- echo "GitLab CI/CD is working"
- whoami
- pwd
💡 文件名的由来:
CI= Continuous Integration(持续集成),yml/yaml是一种配置文件的格式。
二、.gitlab-ci.yml 文件到底怎么写?
2.0 先学一点点 YAML 语法(5 分钟就够)
.gitlab-ci.yml 用的是 YAML 格式,看懂以下三点就够了:
# ① 键值对:用冒号分隔
name: hello-world # 键是 name,值是 hello-world
# ② 列表:用短横线
fruits:
- apple # 列表第 1 项
- banana # 列表第 2 项
- orange # 列表第 3 项
# ③ 嵌套:用缩进(2 个空格!不能多不能少)
person:
name: 张三
hobbies:
- 游泳
- 编程
address: # 又可以继续嵌套
city: 北京
street: 长安街
⚠️ 缩进必须用空格,不能用 Tab! 这是新手最容易犯的错误。
2.1 GitLab 如何"读懂"这个文件?
GitLab 读 .gitlab-ci.yml 时遵循一条核心规则:
🔑 除了 GitLab 预设的保留字,顶层所有键都被当成 Job 名称
来看看具体是什么意思:
stages: # ← GitLab 说:"stages 是我认识的保留字,这是全局配置"
- build
- test
variables: # ← GitLab 说:"variables 也是我认识的保留字"
NAME: world
hello: # ← GitLab 说:"hello?不认识,那你一定是一个 Job!"
script: echo "hello" # 于是 GitLab 按 Job 的规则来解析它
全局保留字(放在最外层,控制整个流水线)
| 保留字 | 干嘛用的 | 大白话 |
|---|---|---|
stages | 定义有哪些阶段,以及执行顺序 | "我的流水线分 3 步:先构建、再测试、最后部署" |
variables | 定义全局变量,所有 Job 都能用 | "所有人的名字都用这个值" |
default | 默认配置,所有 Job 自动继承 | "没特别说明的话,都用这个 Runner 来跑" |
include | 引入另一个 YAML 文件的内容 | "这份配置文件太长,拆成几份" |
workflow | 控制整个流水线要不要运行 | "如果是 MR 就不用跑流水线了" |
cache | 全局缓存,让构建更快 | "上次下载的依赖包别删,下次还能用" |
image | 全局默认的 Docker 镜像 | "所有人都用 Go 1.25 这个环境" |
before_script | 每个 Job 开工前先跑的命令 | "干活前先打扫一下卫生" |
after_script | 每个 Job 干完后跑的命令(哪怕失败了也跑) | "干完活把工具收好" |
Job 内部保留字(放在 Job 里面,控制这个 Job 的行为)
| 保留字 | 干嘛用的 | 大白话 | 必须吗? |
|---|---|---|---|
script | 要执行的命令 | "要干的活儿" | ✅ 必须 |
stage | 属于哪个阶段 | "在哪个车间干活" | 推荐 |
tags | 指定哪个 Runner 来跑 | "派哪个机器人去" | 推荐 |
image | 这个 Job 用哪个 Docker 镜像 | "用什么工具包" | 否 |
variables | 这个 Job 自己用的变量 | "我自己的特殊配置" | 否 |
artifacts | 产出物,传给后面的 Job | "做好的半成品放这儿" | 否 |
environment | 部署到哪个环境 | "送到生产环境还是测试环境" | 否 |
rules | 什么条件下才执行 | "只在 main 分支时才干活" | 否 |
only/except | 旧版条件控制(推荐用 rules 代替) | "只在 main 分支干,不要在 dev 分支干" | 否 |
needs | 要等其他哪些 Job 完成 | "等冲压车间做完我就开工" | 否 |
when | 什么时候执行 | "等我手动点按钮你再跑" | 否 |
cache | 这个 Job 的缓存 | "我的小工具箱" | 否 |
retry | 失败后重试几次 | "出错了再试两次" | 否 |
timeout | 最多跑多久 | "超过 1 小时就别干了" | 否 |
allow_failure | 失败了也没关系 | "你失败不影响其他人" | 否 |
dependencies | 只接收指定 Job 的制品 | "我只要冲压车间的东西,焊接车间的不要" | 否 |
coverage | 从输出中提取测试覆盖率 | "告诉我测试覆盖了百分之几" | 否 |
2.2 script — 最核心的关键字
script 是每个 Job 的心脏,里面是你真正想做的事——一系列 Shell 命令。你可以把平时在终端里敲的命令直接写进去。
my-job:
script:
- echo "第一步:打印信息"
- go build -o myapp ./cmd/server/ # 编译 Go 代码
- go test ./... # 运行测试
- ls -la # 看看生成了什么文件
每一行 - xxx 就是一条命令,GitLab 会按顺序逐条执行。如果某条命令报错了(退出码不是 0),整个 Job 就失败了。
常见误区:script 里面的命令是列表,每条命令独立执行。下面的写法是错的:
# ❌ 错误:cd 到另一个目录只有那一行生效
script:
- cd /tmp
- ls # 这里 ls 的还是原来的目录,不是 /tmp!
如果需要多条命令共享相同的工作目录,要么写在同一行,要么用 pushd/popd:
# ✅ 写法一:用 && 串起来
script:
- cd /tmp && ls
# ✅ 写法二:GitLab 提供的方式(推荐)
script:
- |
cd /tmp
ls
echo "还在 /tmp 目录"
💡 用
|表示多行文本块,里面所有行作为一个整体执行。
2.3 tags — 派哪个 Runner 干活?
Runner 是真正干活的机器。每台 Runner 注册时会给它打标签(tag),就像给机器人贴标签:
Runner A: 标签 [ubuntu-24.04, docker, large-memory]
Runner B: 标签 [macos, ios-build]
Runner C: 标签 [ubuntu-24.04, shell]
写 tags 就是告诉 GitLab:"找一台贴了这个标签的 Runner 来跑":
build:
tags:
- ubuntu-24.04 # GitLab 会去找有 ubuntu-24.04 标签的 Runner
🔍 怎么知道有哪些标签可用? 去 GitLab 页面:Settings → CI/CD → Runners,可以看到所有 Runner 及其标签。
2.4 stage — 在哪个步骤?按什么顺序?
还记得工厂流水线吗?stage 就是告诉 GitLab 这个 Job 在流水线的哪个位置。
stages: # ① 先定义全局执行顺序
- 备料
- 加工
- 质检
- 发货
切菜-job:
stage: 备料 # ② 每个 Job 指定自己在哪一步
炒菜-job:
stage: 加工 # 这个 Job 要等"备料"阶段全部完成才能开始
执行规则:
- 同一
stage里的 Job 同时开工(并行) - 下一个
stage必须等上一个全部完成才能开始 - 上一个
stage有 Job 失败了,后面的默认不执行
💡 Stage 名称可以随便取!
build、test、deploy只是大家的习惯叫法,你用中文构建、测试、部署,或者步骤一、步骤二都行。唯一要求:Job 里的stage: xxx必须和stages列表里的某个名称完全一致。另外,执行顺序由列表的先后位置决定,和名字叫什么无关——写在最前面的先执行。
2.5 variables — 用变量,少写重复内容
变量让你在一处定义值,到处引用。GitLab 有两类变量:
# ① 你自己定义的变量
variables:
APP_NAME: "my-cool-app" # 定义一个变量
DEPLOY_USER: "admin"
deploy:
script:
- echo "正在部署 ${APP_NAME}" # 引用变量用 ${变量名} 或 $变量名
- scp bin/${APP_NAME} ${DEPLOY_USER}@server:/opt/
# ② GitLab 自带的预定义变量(不用定义,直接用)
# $CI_COMMIT_BRANCH → 当前分支名(如 "main")
# $CI_COMMIT_SHA → 当前提交的哈希值(如 "a1b2c3d4")
# $CI_PROJECT_DIR → Runner 上项目的路径
# $CI_JOB_ID → 当前 Job 的编号
⚠️ 重要:密码、Token 等敏感信息不要写在文件里!去 GitLab → Settings → CI/CD → Variables 添加,并勾选 Masked(隐藏显示)。
2.6 when — 控制执行时机
这个关键字控制 Job 什么时候执行。
| 值 | 效果 | 什么时候用? |
|---|---|---|
on_success | 前面都成功才执行(默认值,不写就等于这个) | 正常流程 |
manual | 在 GitLab 网页上点按钮才执行 | ⭐ 部署生产环境!防止误操作 |
delayed | 等一段时间后自动执行 | 给缓存时间,或等 DNS 生效 |
always | 不管前面成功失败都执行 | 清理工作、发通知 |
on_failure | 前面失败了才执行 | 出问题后自动发告警 |
never | 永远不执行 | 配合 rules 临时禁用某个 Job |
# 实战示例
deploy-to-production:
stage: deploy
when: manual # ← 必须手动点按钮,防止不小心部署到生产环境
script:
- echo "部署到生产环境..."
cleanup:
stage: cleanup
when: always # ← 不管成败都清理
script:
- rm -rf /tmp/build-*
notify-on-failure:
stage: notify
when: on_failure # ← 只有出问题了才发通知
script:
- echo "构建失败了!"
2.7 rules — 精确控制"什么时候才干活"
rules 是最强大的条件控制。它按顺序检查条件,匹配到第一条就停。
deploy:
rules:
# 条件 1:如果是 main 分支 → 执行
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
# 条件 2:如果是 MR(合并请求) → 手动触发
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
# 条件 3:如果改动了 Go 文件 → 执行
- changes:
- "**/*.go"
- go.mod
when: on_success
# 条件 4:以上都不满足 → 不执行
- when: never
常用场景速查:
# 场景 1:只在 main 分支自动部署
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
# 场景 2:只在 MR 时运行测试
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# 场景 3:打 Tag 时触发发布
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' # 匹配 v1.2.3 这样的版本号
# 场景 4:只改动了 .go 文件才触发
rules:
- changes:
- "**/*.go"
# 场景 5:工作日才运行(节省资源)
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: '$CI_COMMIT_BRANCH == "main"'
📖 常用预定义变量:
| 变量 | 值举例 | 含义 |
|---|---|---|
$CI_COMMIT_BRANCH | main | 当前分支名 |
$CI_COMMIT_TAG | v1.0.0 | Tag 名(只在打 Tag 时有值) |
$CI_PIPELINE_SOURCE | push / merge_request_event / schedule / web | 谁触发了流水线 |
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME | feature/login | MR 的来源分支 |
$CI_MERGE_REQUEST_TARGET_BRANCH_NAME | main | MR 要合并到的目标分支 |
$CI_COMMIT_MESSAGE | fix: 修复登录 bug | 提交信息 |
2.8 ⚠️ 重要:分支和 Tag 不能同时判断
很多新手想写"只在 main 分支且打了 tag 时才部署",但这是做不到的——因为 $CI_COMMIT_BRANCH 和 $CI_COMMIT_TAG 永远不会同时有值:
| 你推送的是... | $CI_COMMIT_BRANCH | $CI_COMMIT_TAG |
|---|---|---|
| 代码提交(如 main 分支) | main | 空 |
版本 Tag(如 v1.0.0) | 空 | v1.0.0 |
Tag 和分支是两种独立的事件,一次推送要么是提交代码(有分支名),要么是打 Tag(有 Tag 名),不可能同时发生。
那怎么办?——分开控制
实际项目中的做法是:main 分支负责 CI(编译测试),Tag 负责 CD(部署发布):
stages:
- build
- test
- deploy
# main 分支推送 → 自动编译
build:
stage: build
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- go build -o bin/app ./cmd/server/
# main 分支推送 → 自动测试
test:
stage: test
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- go test ./...
# 打了版本 Tag → 才部署到生产环境
deploy:
stage: deploy
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/'
script:
- echo "发布版本 $CI_COMMIT_TAG 到生产环境"
日常流程:
开发阶段:
推送代码到 main → 自动跑 build + test ✅
准备发布:
git tag v1.0.0
git push origin v1.0.0
→ 自动跑 deploy ✅(只跑 deploy,不跑 build/test)
💡 如果确实需要确保 Tag 一定打在 main 分支上,可以在 deploy Job 的 script 里加校验:
- git branch -r --contains $CI_COMMIT_SHA | grep "origin/main" || { echo "❌ 这个 tag 不在 main 分支上!"; exit 1; }
三、核心实战:CI 和 CD 到底怎么做?
前面学了一大堆关键字,现在我们把它们串起来,看一个真实项目是怎么用的。
3.1 CI(持续集成)— 自动编译 + 自动测试
目的:每次推送代码,自动检查"代码能不能编译通过?测试有没有挂?"
# ==============================
# 只做 CI,不做部署的配置
# ==============================
stages:
- build # 第一阶段:编译
- test # 第二阶段:测试
# ---------- Job 1: 编译 ----------
build:
stage: build # 属于 build 阶段
tags:
- ubuntu-24.04 # 用这台机器跑
script:
- go build -o bin/server ./cmd/server/
artifacts: # 把编译结果保存下来
paths:
- bin/ # 保存 bin 文件夹
expire_in: 1 day # 一天后自动删除
# ---------- Job 2: 测试 ----------
test:
stage: test # 属于 test 阶段(等 build 完成再跑)
tags:
- ubuntu-24.04
script:
- go test -v ./... # -v 显示详细输出
needs: # 只需要 build 完成就行
- build
流程图:
你推送代码到 GitLab
│
▼
┌───────────────────┐
│ Stage 1: build │ 运行 go build,如果能编译通过 → ✅
│ Job: build │ 生成 bin/server 文件,保存为 artifacts
└───────┬───────────┘
│ build 成功
▼
┌───────────────────┐
│ Stage 2: test │ 运行 go test,测试全通过 → ✅
│ Job: test │ 如果有测试失败 → ❌,你会收到邮件通知
└───────────────────┘
3.2 CD(持续部署)— 自动部署到服务器
目的:测试通过后,把程序自动(或手动确认后)部署到服务器。
CD 分两种方式:
| 方式 | 触发方式 | 风险 | 适合场景 |
|---|---|---|---|
| 持续交付 (Continuous Delivery) | 测试通过后,手动点按钮才部署 | 低 ✅ | 生产环境 |
| 持续部署 (Continuous Deployment) | 测试通过后,自动部署 | 高 ⚠️ | 测试环境 |
stages:
- build
- test
- deploy-staging # 预发布环境
- deploy-production # 生产环境
# ... build 和 test 同上 ...
# ---------- 部署到预发布(自动) ----------
deploy-staging:
stage: deploy-staging
tags:
- ubuntu-24.04
environment: # 关联环境
name: staging # 环境名称
url: https://staging.example.com
script:
- echo "自动部署到预发布环境..."
- ./deploy.sh staging
only:
- develop # 只在 develop 分支时自动部署
# ---------- 部署到生产环境(手动) ----------
deploy-production:
stage: deploy-production
tags:
- ubuntu-24.04
when: manual # ⭐ 手动触发!防止误操作
environment:
name: production
url: https://example.com
script:
- echo "部署到生产环境(管理员已确认)..."
- ./deploy.sh production
only:
- main # 只允许从 main 分支部署
3.3 artifacts — Job 之间如何传递文件
artifacts 就是一个 Job 的"产出物"——它做出来的东西,后面的 Job 可以直接拿来用。
类比:build Job 做了一个面包,把面包放在货架上(artifacts),test Job 从货架上取面包来检查质量(needs 或自动继承)。
build:
stage: build
script:
- go build -o bin/myapp ./cmd/server/
artifacts:
paths: # 把哪些文件/文件夹放上货架
- bin/
name: "build-$CI_COMMIT_SHORT_SHA" # 给制品起个名
expire_in: 1 week # 一周后自动清理
when: on_success # 只在成功时保存(默认)
deploy:
stage: deploy
needs: [build] # 告诉 GitLab:"我需要 build 的 artifacts"
script:
- ls bin/ # 能看到 bin/myapp 这个文件!
- scp bin/myapp user@server:/opt/
⚠️ 注意事项:
- 不用
needs的话,一个 Stage 会自动接收上一个 Stage 所有 Job的 artifacts- 用
needs的话,只会接收你指定的那些 Job的 artifacts- artifacts 会占用存储空间,记得设
expire_in
💡 文档里到处出现的
bin/到底是哪里的目录?
bin/是相对于项目根目录的路径,不是系统根目录的/bin。你的项目被 GitLab Runner clone 后,在 Runner 机器上的实际位置类似:
/home/gitlab-runner/builds/随机字符串/0/你的用户名/你的项目名/ ├── .gitlab-ci.yml ├── go.mod ├── cmd/server/main.go └── bin/ ← go build -o bin/xxx 就生成在这里 └── demo_go整个流程:
① go build -o bin/demo_go → 在项目目录里生成 bin/demo_go ② artifacts: paths: [bin/] → 把项目下的 bin/ 整个上传到 GitLab 保存 ③ 下一个 Job 自动下载 artifacts → bin/ 又出现在项目目录里 ④ scp bin/demo_go 目标服务器 → 从项目目录传走
bin/这个名字可以随便改——比如叫output/、dist/,只是一个装了编译产物的普通文件夹。
3.4 environment — 在 GitLab 里管理部署历史
用了 environment 后,GitLab 网页上会多出一个"环境"页面,显示:
- 📜 每次部署的历史记录
- 🔍 当前部署的是哪个 commit
- ⏪ 一键回滚按钮
- 🟢/🔴 环境是否正常运行
deploy:
stage: deploy
environment:
name: production # 环境名称(必填)
url: https://myapp.example.com # 访问地址(选填,但建议填)
on_stop: stop-production # 关联一个"销毁环境"的 Job
# 停止/销毁环境的 Job(手动触发)
stop-production:
stage: deploy
environment:
name: production
action: stop # 表示这是"停止环境"的操作
when: manual
script:
- echo "关闭生产环境..."
3.5 needs — 谁说一定要按顺序来?
默认规则是必须一个 Stage 完成才开始下一个。needs 让你打破这个规则:
stages:
- build
- test
- deploy
build:
stage: build
# ... 编译要 5 分钟 ...
fast-test: # 简单测试,不需要编译产物
stage: test
needs: [] # ← 空的!不等 build,立即开始!
script:
- echo "只检查代码风格,不需要编译"
- golangci-lint run
slow-test: # 集成测试,需要编译产物
stage: test
needs: [build] # ← 等 build 完成
script:
- ./bin/server &
- curl http://localhost:8080/
quick-deploy: # 部署到测试环境
stage: deploy
needs: [build] # ← 只等 build,不等任何 test!
script:
- echo "快速部署到测试环境"
效果:快速测试 + 编译 + 快速部署 可以同时进行,大大缩短等待时间!
四、部署到远程服务器:Runner 做好的程序怎么送到线上?
前面讲了 CD 的概念、environment 关键字,但最终要解决一个问题:
Runner 上编译好的二进制文件,怎么传到目标服务器并跑起来?
整体流程:
Runner 服务器 目标服务器 (测试/正式)
┌──────────────┐ ┌──────────────┐
│ 编译 Go │ │ │
│ 生成二进制 │ ── 网络传输 ──→ │ 接收文件 │
│ │ │ 重启服务 │
│ (CI 完成) │ │ (CD 完成) │
└──────────────┘ └──────────────┘
4.1 前置准备:打通 Runner 到目标服务器的 SSH
不管用哪种传输方式,第一步都是让 Runner 能免密码 SSH到目标服务器。
① 在 Runner 机器上生成密钥
# 用 gitlab-runner 用户生成 SSH 密钥
sudo -u gitlab-runner ssh-keygen -t ed25519 -C "gitlab-runner" -f /home/gitlab-runner/.ssh/id_ed25519 -N ""
# 查看公钥(马上要用)
sudo cat /home/gitlab-runner/.ssh/id_ed25519.pub
② 把公钥放到目标服务器
在目标服务器上执行:
# 把 Runner 的公钥追加到 authorized_keys
echo "ssh-ed25519 AAAAC3... gitlab-runner" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
③ 测试连通 + 消除指纹确认
# 在 Runner 上测试
sudo -u gitlab-runner ssh-keyscan 目标服务器IP >> /home/gitlab-runner/.ssh/known_hosts
sudo -u gitlab-runner ssh 用户名@目标服务器IP "echo 连接成功"
4.2 方式一:scp + ssh(最简单,推荐新人用)
直接把一个文件拷过去,然后远程执行重启命令。适合只部署单个二进制文件的场景。
# 在 GitLab CI/CD Variables 中配置以下变量:
# STAGING_HOST: 测试服务器 IP
# STAGING_USER: 测试服务器用户名
# PROD_HOST: 正式服务器 IP
# PROD_USER: 正式服务器用户名
deploy-staging:
stage: deploy
tags:
- ubuntu-24.04
environment:
name: staging
url: http://${STAGING_HOST}:8080
script:
# ① 拷贝文件
- scp ${APP_NAME} ${STAGING_USER}@${STAGING_HOST}:/opt/${APP_NAME}/
# ② 远程重启服务
- ssh ${STAGING_USER}@${STAGING_HOST} "sudo systemctl restart ${APP_NAME}"
# ③ 等 2 秒检查服务是否正常
- sleep 2
- ssh ${STAGING_USER}@${STAGING_HOST} "systemctl is-active ${APP_NAME}"
💡 关键点:变量
STAGING_HOST、PROD_HOST等建议在 GitLab 网页 Settings → CI/CD → Variables 中设置,不要硬编码在文件里。
4.3 方式二:rsync 增量同步(适合多文件、大项目)
如果需要同步的不仅是二进制文件,还有配置文件、静态页面等,用 rsync 只传有变化的部分,比 scp 快很多。
deploy:
stage: deploy
script:
# -a 归档模式 -v 显示详情 -z 压缩 --delete 删除目标多余文件
- rsync -avz --delete \
${APP_NAME} \
config/ \
static/ \
${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/
- ssh ${PROD_USER}@${PROD_HOST} "sudo systemctl restart ${APP_NAME}"
4.4 方式三:Docker 镜像部署(更标准、更隔离)
如果目标服务器装了 Docker,可以把程序打包成镜像,推送过去运行。好处是环境完全一致,不会出现"我这能跑你那不行"。
docker-build-push:
stage: build
script:
- docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} .
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
deploy-docker:
stage: deploy
script:
- ssh ${PROD_USER}@${PROD_HOST} "
docker pull ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} &&
docker stop ${APP_NAME} || true &&
docker rm ${APP_NAME} || true &&
docker run -d --name ${APP_NAME} -p 8080:8080 ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
"
4.5 目标服务器上如何管理服务?(systemd)
文件传过去后,需要一个"管家"来保证程序一直在后台运行、崩溃自动重启。Linux 上最常用的是 systemd。
在目标服务器上创建服务文件 /etc/systemd/system/demo_go.service:
[Unit]
Description=demo_go HTTP Server # 服务描述
After=network.target # 网络就绪后再启动
[Service]
Type=simple
User=deploy # 用哪个用户运行(不要用 root)
WorkingDirectory=/opt/demo_go
ExecStart=/opt/demo_go/demo_go # 二进制文件的路径
Restart=always # 崩溃了自动重启
RestartSec=5 # 等 5 秒再重启
Environment="PORT=8080" # 环境变量
[Install]
WantedBy=multi-user.target
常用管理命令:
sudo systemctl daemon-reload # 修改配置文件后重新加载
sudo systemctl enable demo_go # 开机自启
sudo systemctl start demo_go # 启动
sudo systemctl stop demo_go # 停止
sudo systemctl restart demo_go # 重启
sudo systemctl status demo_go # 查看状态和日志
4.6 三种部署方式对比
| 方式 | 难度 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| scp + ssh | ⭐ 简单 | 单文件、小项目 | 配置最少,开箱即用 | 每次传全量文件 |
| rsync + ssh | ⭐⭐ 一般 | 多文件、静态资源 | 增量同步,速度快 | 需要额外安装 rsync |
| Docker 镜像 | ⭐⭐⭐ 较难 | 微服务、多环境 | 环境完全隔离 | 需要 Docker 环境 |
对于你当前的
demo_go项目,scp + ssh + systemd 完全够用。
五、版本回退:部署出问题了怎么办?
部署不是一锤子买卖,万一新版本有 bug,必须能快速回退到上一个正常版本。
回退的核心思路就四个字:先备后换。
5.1 最简单的方式:保留旧文件的备份
每次部署时,先把旧文件改个名,再放新的。出问题了就把备份改回来。
deploy:
stage: deploy
tags:
- ubuntu-24.04
environment:
name: production
script:
# ① 远程备份旧版本(加上时间戳)
- ssh ${PROD_USER}@${PROD_HOST} "
if [ -f /opt/${APP_NAME}/${APP_NAME} ]; then
cp /opt/${APP_NAME}/${APP_NAME} /opt/${APP_NAME}/${APP_NAME}.bak.$(date +%Y%m%d_%H%M%S);
fi
"
# ② 传新版本
- scp ${APP_NAME} ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/
# ③ 重启服务,如果失败就自动回滚
- |
ssh ${PROD_USER}@${PROD_HOST} "sudo systemctl restart ${APP_NAME}"
if [ $? -ne 0 ]; then
echo "❌ 部署失败!自动回滚..."
LATEST_BAK=$(ssh ${PROD_USER}@${PROD_HOST} "ls -t /opt/${APP_NAME}/${APP_NAME}.bak.* | head -1")
ssh ${PROD_USER}@${PROD_HOST} "cp ${LATEST_BAK} /opt/${APP_NAME}/${APP_NAME} && sudo systemctl restart ${APP_NAME}"
exit 1
fi
- echo "✅ 部署成功"
5.2 推荐方式:用 GitLab 的 Rollback 按钮
GitLab 的 environment 功能自带一键回滚——直接用上一次成功的产物重新部署。
deploy:
stage: deploy
tags:
- ubuntu-24.04
environment:
name: production
# 关键:指定回滚时跑哪个 Job
# GitLab 会自动用上一次 deploy Job 的 artifacts 重新执行
script:
# 部署前先备份
- ssh ${PROD_USER}@${PROD_HOST} "
[ -f /opt/${APP_NAME}/${APP_NAME} ] && cp /opt/${APP_NAME}/${APP_NAME} /opt/${APP_NAME}/${APP_NAME}.bak || true
"
- scp ${APP_NAME} ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/
- ssh ${PROD_USER}@${PROD_HOST} "sudo systemctl restart ${APP_NAME}"
在 GitLab 网页上回滚的路径:
你的项目 → Deployments → Environments → production
→ 点击要回滚到的那个版本 → 点击 "Rollback environment" 按钮
GitLab 会自动:
- 找到那次部署用的 artifacts(编译产物)
- 用那些 artifacts 重新跑 deploy Job
- 完成回滚
⚠️ 前提:该版本的 artifacts 还没过期(受
expire_in控制)
5.3 手动回滚 Job(兜底方案)
单独写一个回滚 Job,出问题时手动点击执行:
rollback:
stage: deploy
tags:
- ubuntu-24.04
when: manual # 手动触发
environment:
name: production
action: rollback # 标记为回滚操作
script:
# 找到最新的备份文件
- LATEST_BAK=$(ssh ${PROD_USER}@${PROD_HOST} "ls -t /opt/${APP_NAME}/${APP_NAME}.bak.* 2>/dev/null | head -1")
- |
if [ -z "${LATEST_BAK}" ]; then
echo "❌ 没有可用的备份文件!"
exit 1
fi
- echo "回滚到备份: ${LATEST_BAK}"
- ssh ${PROD_USER}@${PROD_HOST} "
cp ${LATEST_BAK} /opt/${APP_NAME}/${APP_NAME} &&
sudo systemctl restart ${APP_NAME}
"
- echo "✅ 回滚完成"
5.4 进阶:多版本保留策略
保留最近 N 个版本,方便选择回退到任意版本:
deploy:
stage: deploy
script:
# ① 把新文件命名为带版本号的文件
- scp ${APP_NAME} ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/releases/${CI_COMMIT_SHORT_SHA}/
# ② 更新一个软链接指向当前版本
- ssh ${PROD_USER}@${PROD_HOST} "
ln -sfn /opt/${APP_NAME}/releases/${CI_COMMIT_SHORT_SHA}/${APP_NAME} /opt/${APP_NAME}/current &&
sudo systemctl restart ${APP_NAME}
"
# ③ 只保留最近 5 个版本,旧的删掉
- ssh ${PROD_USER}@${PROD_HOST} "
cd /opt/${APP_NAME}/releases && ls -t | tail -n +6 | xargs -r rm -rf
"
目录结构:
/opt/demo_go/
├── current → releases/a1b2c3d/demo_go # 软链接,指向当前版本
└── releases/
├── a1b2c3d/demo_go # 版本 1
├── e4f5g6h/demo_go # 版本 2
└── i7j8k9l/demo_go # 版本 3(最新)
回退时只需要改软链接:
# 回退到版本 e4f5g6h
ln -sfn /opt/demo_go/releases/e4f5g6h/demo_go /opt/demo_go/current
systemctl restart demo_go
5.5 回退策略对比
| 方式 | 实现难度 | 回退速度 | GitLab UI 支持 | 适合场景 |
|---|---|---|---|---|
| 备份旧文件 | ⭐ 简单 | 快 | 无 | 应急兜底 |
| GitLab Rollback 按钮 | ⭐⭐ 一般 | 快(需重新跑 Job) | ✅ 原生支持 | 常规使用 |
| 手动回滚 Job | ⭐⭐ 一般 | 快 | 半支持 | 备份兜底 |
| 多版本软链接 | ⭐⭐⭐ 较难 | 秒级 | 无 | 需要秒级回退的大项目 |
💡 建议组合使用:GitLab Rollback(主力) + 备份文件回滚(兜底)。
六、你的 demo_go 项目 — 完整 CI/CD 配置
针对你当前的 Go HTTP 服务,这是一个可以直接用的配置文件:
# ============================================
# .gitlab-ci.yml — demo_go 项目
# 功能:编译 → 测试 → 手动部署
# ============================================
# 第一步:定义流水线有哪些阶段(按顺序执行)
stages:
- build
- test
- deploy
# 第二步:定义全局变量(所有 Job 都能用)
variables:
GO_VERSION: "1.25"
APP_NAME: "demo_go"
# ==================== 构建 ====================
build:
stage: build # 属于"构建"阶段
tags:
- ubuntu-24.04 # 用贴了这个标签的 Runner
script:
# -ldflags="-s -w" 可以减小编译出来的文件体积
- go build -ldflags="-s -w" -o bin/${APP_NAME} ./cmd/server/
- echo "构建完成!文件大小:"
- ls -lh bin/${APP_NAME}
artifacts: # 编译产物保存下来
paths:
- bin/
expire_in: 1 day # 一天后自动删除
# ==================== 测试 ====================
test:
stage: test # 属于"测试"阶段(等 build 完成)
tags:
- ubuntu-24.04
script:
# -race 检测并发问题,-coverprofile 生成覆盖率报告
- go test -v -race -coverprofile=coverage.out ./...
# 打印每个函数的测试覆盖率
- go tool cover -func=coverage.out
coverage: '/total:.*?(\d+\.\d+)%/' # GitLab 自动提取覆盖率数字
artifacts:
reports:
coverage_report: # 让 GitLab 解析覆盖率
coverage_format: cobertura
path: coverage.out
# ==================== 部署(手动触发) ====================
deploy:
stage: deploy
tags:
- ubuntu-24.04
when: manual # ⭐ 必须手动点按钮才部署
environment:
name: production
script:
- echo "正在部署 ${APP_NAME} 到生产环境..."
- sudo cp bin/${APP_NAME} /opt/${APP_NAME}/
- sudo systemctl restart ${APP_NAME}
- echo "部署完成!"
only:
- main # 只允许从 main 分支部署
七、从零到一:第一次跑流水线的完整步骤
第 1 步:写好 .gitlab-ci.yml
把上面的文件内容放到你项目的根目录,文件名必须是 .gitlab-ci.yml。
第 2 步:推送到 GitLab
git add .gitlab-ci.yml
git commit -m "添加 CI/CD 配置"
git push origin main
第 3 步:去 GitLab 网页上看结果
- 打开你的 GitLab 项目页面
- 点击左侧菜单:CI/CD → Pipelines
- 你会看到一条新的流水线在运行 —— 有绿色 ✅ 就是成功,红色 ❌ 就是失败
- 点击流水线,再点击某个 Job,能看到实时日志
GitLab 页面路径:
你的项目 → CI/CD → Pipelines → 点击某条流水线 → 点击某个 Job → 看日志
第 4 步:手动触发部署
如果 configure 里有 when: manual 的 Job:
- 在流水线页面,你会看到一个播放按钮 ▶️(而不是自动执行)
- 点击它,部署就开始执行了
八、Runner 那些事
什么是 Runner?
Runner 就是真正干活的那台机器。GitLab 本身只负责调度,不负责执行——它把任务派给 Runner,Runner 干完活把结果回报给 GitLab。
你的代码 → GitLab(大脑,负责指挥)
│
▼
Runner(手脚,负责干活)
│
▼
执行 go build、go test...
你项目用的 Shell Executor
你的 Runner 用的是 Shell Executor,意味着 Job 直接在 Runner 机器的 shell 里执行。
# 在 Runner 机器上,可以用这些命令管理
gitlab-runner status # 看 Runner 是否在运行
gitlab-runner list # 列出已注册的 Runner
gitlab-runner verify # 检查 Runner 连接是否正常
cat /etc/gitlab-runner/config.toml # 查看 Runner 配置
常见坑 ⚠️
根据你之前的排查经验:
| 问题 | 现象 | 解决 |
|---|---|---|
.bash_logout 干扰 | Job 报 prepare environment: exit status 1 | 清空 /home/gitlab-runner/.bash_logout |
| 缺少依赖 | go: command not found | 在 Runner 机器上装好 Go |
| 缺少 git | clone 代码失败 | apt-get install -y git |
| 权限不足 | Permission denied | 检查 gitlab-runner 用户的权限 |
九、常见场景速查(拿来就用)
场景 1:只在 MR(合并请求)时跑测试
test:
stage: test
script:
- go test ./...
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
场景 2:打 Tag 时自动发布
release:
stage: deploy
script:
- echo "发布版本 $CI_COMMIT_TAG"
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
场景 3:定时任务(每天晚上跑)
不需要改 .gitlab-ci.yml!去 GitLab:CI/CD → Schedules → New schedule,设置每天凌晨 2 点跑。
场景 4:多个 Go 版本同时测试
test-go-1.24:
stage: test
image: golang:1.24
script:
- go test ./...
test-go-1.25:
stage: test
image: golang:1.25
script:
- go test ./...
场景 5:部署失败自动回滚
deploy:
stage: deploy
script:
- ./deploy.sh
environment:
name: production
on_stop: rollback # 关联回滚 Job
rollback:
stage: deploy
when: manual
environment:
name: production
action: stop
script:
- echo "回滚到上一个版本..."
十、快速参考卡片
┌──────────────────────────────────────────────────────────┐
│ .gitlab-ci.yml 大局观 │
├──────────────────────────────────────────────────────────┤
│ │
│ stages: [...] ← 全局:定义阶段顺序 │
│ variables: {...} ← 全局:定义变量 │
│ default: {...} ← 全局:默认配置 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ my-job-1: ← 自定义 Job │ │
│ │ stage: test ← 在哪个阶段 │ │
│ │ tags: [linux] ← 哪个 Runner 跑 │ │
│ │ script: [...] ← 干什么活(必填!) │ │
│ │ artifacts: {...} ← 产出什么 │ │
│ │ environment: {} ← 部署到哪 │ │
│ │ rules: [...] ← 什么时候触发 │ │
│ │ when: manual ← 自动还是手动 │ │
│ │ needs: [job-x] ← 等谁完成 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ my-job-2: ← 另一个 Job │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
我想做... → 用什么关键字
| 我想... | 关键字 |
|---|---|
| 控制执行顺序 | stages + stage + needs |
| 指定哪台机器跑 | tags |
| 只在特定分支运行 | rules + $CI_COMMIT_BRANCH |
| 手动点了才执行 | when: manual |
| 把构建结果传给下一步 | artifacts |
| 部署后能在网页看到历史 | environment |
| 密码/Token 不写在文件里 | GitLab UI → Settings → CI/CD → Variables |
| 下载依赖更快 | cache |
| 失败了自动重试 | retry |
| 某个 Job 失败不影响整体 | allow_failure: true |
| 定时自动跑 | GitLab UI → CI/CD → Schedules(无需改文件) |
| 配置文件太大拆分 | include |
十一、下一步可以做什么?
- 动手试试:把第六章的完整配置复制到你的
.gitlab-ci.yml,推送看效果 - 装一个 Runner:如果还没有 Runner,在自己的服务器上装
gitlab-runner并注册 - 设置变量:把服务器的 SSH 密钥、部署密码等放到 GitLab Variables 中
- 加上通知:在
after_script里加企业微信/钉钉/Slack 通知 - 加上代码检查:增加
golangci-lintJob,自动检查代码质量