14. CI/CD 流水线中集成 Docker:GitHub Actions 自动构建部署
每次改完代码,你都要经历这套"手动三连":本地
docker build→docker push→ SSH 到服务器docker pull+docker compose up -d。改一行代码,操作五分钟。要是哪天忘了推镜像、忘了重启容器,线上跑的还是老版本,排查半天才发现"哦,我没部署"。这种日子,该结束了。CI/CD 就是来帮你把这些重复操作全自动化的------代码一推,镜像自动构建、自动推送、自动部署,你只管写代码就行。

▲ 把"手动三连"交给流水线:代码一推,构建、测试、部署一条龙自动跑完。
一、什么是 CI/CD?Docker 在里面扮演什么角色?
1.1 一分钟搞懂 CI/CD
CI(持续集成) 和 CD(持续部署/交付) 听起来高大上,但核心思想很朴素:
| 概念 | 全称 | 一句话解释 |
|---|---|---|
| CI | Continuous Integration | 代码一推送到仓库,自动跑测试、自动构建产物 |
| CD | Continuous Deployment | 构建完自动部署到服务器,中间不用人插手 |
串起来就是:代码提交 → 自动测试 → 自动构建镜像 → 自动推送 → 自动部署。整条链路无人值守,跑完线上就是最新的。
1.2 Docker 在 CI/CD 中的角色
Docker 是这条流水线里的"标准化打包环节":
代码提交 → [自动测试] → [Docker 构建镜像] → [推送镜像到仓库] → [服务器拉取并部署]
↑ ↑ ↑
你的 Dockerfile Docker Hub / ACR docker compose up

▲ 每一步都由流水线自动接力,你只需要把代码推上去。
为什么用 Docker 做 CI/CD 特别合适? 因为镜像天然就是"不可变产物"------同一个 Dockerfile + 同一份代码,在任何机器上构建出来的镜像都一模一样。这保证了"开发能跑,生产也能跑"。
1.3 常见的 CI/CD 工具
| 工具 | 特点 | 适合场景 |
|---|---|---|
| GitHub Actions | 免费额度大、跟 GitHub 深度集成、YAML 配置 | 开源项目、个人项目、中小团队 |
| GitLab CI | 内置于 GitLab、Pipeline 概念强大 | 用 GitLab 的团队 |
| Jenkins | 老牌王者、插件生态丰富 | 大型企业、复杂流水线 |
| 阿里云效 / 腾讯云 CODING | 国内云厂商方案、中文友好 | 国内团队、需要国内网络加速 |
本文以 GitHub Actions 为例,因为它免费、配置简单、跟 GitHub 仓库无缝集成,上手最快。文章末尾也会简要介绍 Jenkins 和 GitLab CI 的等效方案。
二、GitHub Actions 基础概念
在动手写配置之前,先搞清楚几个核心概念:
| 概念 | 解释 | 类比 |
|---|---|---|
| Workflow | 一个 .yml 文件就是一个工作流,定义在 .github/workflows/ 目录下 |
一份"施工图纸" |
| Event(事件) | 触发工作流的条件,比如 push、pull_request、打 tag | "开工信号" |
| Job(作业) | 工作流里的一组步骤,一个工作流可以有多个 Job | "施工阶段" |
| Step(步骤) | Job 里的单个操作,可以是运行命令或调用现成的 Action | "具体动作" |
| Action | 社区共享的可复用步骤(类似于 Docker Hub 上的镜像) | "预制件" |
| Runner | 执行 Job 的虚拟机(GitHub 免费提供 Linux/Mac/Windows) | "施工队" |
用一个流程图理解它们的关系:
Event(push 到 main)
│
▼
Workflow(docker-build.yml)
│
├── Job 1: build-and-push
│ ├── Step 1: checkout 代码
│ ├── Step 2: 登录 Docker Hub
│ ├── Step 3: 构建镜像
│ └── Step 4: 推送镜像
│
└── Job 2: deploy(依赖 Job 1 完成)
├── Step 1: SSH 到服务器
└── Step 2: docker compose up -d
三、实战:编写完整的 GitHub Actions 工作流
3.1 项目结构
假设你有一个简单的 Node.js 项目(换成 Python、Java 都一样,改 Dockerfile 就行):
my-app/
├── .github/
│ └── workflows/
│ └── docker-build.yml ← 工作流文件
├── src/
│ └── index.js
├── docker-compose.yml
├── Dockerfile
├── package.json
└── .dockerignore
3.2 Dockerfile(构建目标)
FROM node:20-alpine
WORKDIR /app
# 先复制依赖文件,利用 Docker 缓存层
COPY package.json package-lock.json ./
RUN npm ci --production
# 再复制源码
COPY src/ ./src/
EXPOSE 3000
CMD ["node", "src/index.js"]
3.3 完整的工作流文件
这是本文的核心产出------一个可以直接拿去用的 GitHub Actions 配置:
# .github/workflows/docker-build.yml
name: Docker Build and Deploy
on:
# 推送到 main 分支时触发
push:
branches: [main]
# 打 tag 时也触发(用于发布正式版本)
tags: ['v*']
# 允许手动触发
workflow_dispatch:
# 定义环境变量
env:
DOCKER_IMAGE: your-dockerhub-username/my-app
# GitHub 自带的容器镜像仓库(可选方案)
GHCR_IMAGE: ghcr.io/${{ github.repository }}
jobs:
# ========== Job 1:构建并推送镜像 ==========
build-and-push:
runs-on: ubuntu-latest
steps:
# Step 1: 拉取代码
- name: Checkout code
uses: actions/checkout@v4
# Step 2: 设置 Docker Buildx(支持高级构建特性)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Step 3: 登录 Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Step 4: 登录 GitHub Container Registry(可选,双推)
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Step 5: 生成镜像标签
- name: Generate image tags
id: meta
run: |
# 获取 Git 短哈希(用于唯一标识每次构建)
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
echo "sha_tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
# 根据触发方式生成不同的标签
if [[ "${{ github.ref_type }}" == "tag" ]]; then
# 打 tag 触发:用版本号 + latest
VERSION=${GITHUB_REF_NAME#v}
echo "tags=${{ env.DOCKER_IMAGE }}:${VERSION},${{ env.DOCKER_IMAGE }}:latest" >> $GITHUB_OUTPUT
echo "env_name=production" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
# push 到 main:用 dev + sha
echo "tags=${{ env.DOCKER_IMAGE }}:dev,${{ env.DOCKER_IMAGE }}:${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "env_name=dev" >> $GITHUB_OUTPUT
fi
# Step 6: 构建并推送镜像(带缓存优化)
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
# 缓存优化:利用 GitHub Actions 缓存
cache-from: type=gha
cache-to: type=gha,mode=max
# ========== Job 2:部署到服务器 ==========
deploy:
needs: build-and-push # 必须等 Job 1 完成
runs-on: ubuntu-latest
# 仅在推送到 main 或打 tag 时部署
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
steps:
- name: Deploy to server via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
script: |
cd /opt/my-app
# 拉取最新镜像
docker compose pull
# 重新创建容器(只替换有变化的服务)
docker compose up -d --remove-orphans
# 清理悬空镜像,释放磁盘空间
docker image prune -f
# 打印当前运行的容器状态
docker compose ps
echo "✅ 部署完成!镜像版本:${{ needs.build-and-push.outputs.image_tag }}"
3.4 镜像标签策略详解
上面用了一段 Shell 脚本来生成标签,这里用表格总结一下策略:
| 触发条件 | 生成的标签 | 用途 |
|---|---|---|
push 到 main 分支 |
dev + a1b2c3d(Git 短哈希) |
开发环境自动部署 |
打 tag v1.2.3 |
1.2.3 + latest |
生产环境正式发布 |
| 手动触发 | 同 push 到 main | 临时构建测试 |
为什么要同时打两个标签?
-
dev/1.2.3:可追踪的固定版本,出问题可以回滚 -
a1b2c3d(Git SHA):精确到"哪次提交构建的",排查问题利器 -
latest:方便本地开发时docker pull拿最新版
四、配置 GitHub Secrets(关键步骤)
工作流里用到了好几个敏感信息,绝对不能硬编码在 YAML 文件里(推到公开仓库就泄露了)。需要用 GitHub Secrets:
4.1 需要配置的 Secrets
| Secret 名称 | 值 | 获取方式 |
|---|---|---|
DOCKERHUB_USERNAME |
你的 Docker Hub 用户名 | 注册 Docker Hub 时的用户名 |
DOCKERHUB_TOKEN |
Docker Hub 访问令牌 | Docker Hub → Account Settings → Security → New Access Token |
SERVER_HOST |
服务器 IP 或域名 | 你的云服务器公网地址 |
SERVER_USER |
SSH 用户名 | 通常是 root 或 ubuntu |
SERVER_SSH_KEY |
SSH 私钥内容 | cat ~/.ssh/id_rsa(服务器配好公钥后) |
4.2 配置步骤
-
打开你的 GitHub 仓库页面
-
进入 Settings → Secrets and variables → Actions
-
点击 New repository secret
-
逐个填入上面的 Secret 名称和值
重要提醒 :Docker Hub 的 Token 权限选择 Read/Write ,否则推送镜像时会报
denied: requested access to the resource is denied。
4.3 服务器端准备
在你的云服务器上,确保以下环境已就绪:
# 1. 安装 Docker 和 Docker Compose(参考本系列第 2 篇)
docker --version
docker compose version
# 2. 登录 Docker Hub(让服务器有拉取私有镜像的权限)
docker login
# 3. 创建应用目录,放好 docker-compose.yml
mkdir -p /opt/my-app
cd /opt/my-app
# 4. 创建 docker-compose.yml(服务器版本)
cat > docker-compose.yml << 'EOF'
version: "3.8"
services:
app:
image: your-dockerhub-username/my-app:dev
container_name: my-app
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=${DB_HOST}
restart: always
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
EOF
五、缓存优化:让构建速度翻倍
5.1 为什么需要缓存?
每次 CI 构建都是在一台全新的虚拟机上跑,没有之前的构建层缓存。一个 Node.js 项目光 npm install 就要 1-2 分钟,Java Maven 项目更夸张------首次构建可能 5-10 分钟。
5.2 GitHub Actions 缓存方案
上面的工作流已经用上了:
cache-from: type=gha
cache-to: type=gha,mode=max
这两行的含义:
| 参数 | 说明 |
|---|---|
type=gha |
使用 GitHub Actions 内置缓存(无需额外配置) |
mode=max |
缓存所有层(不只是最终层),下次构建跳过未变化的层 |
实测效果:
| 场景 | 无缓存 | 有缓存 | 提速 |
|---|---|---|---|
| 仅修改源码(依赖不变) | 2 分 30 秒 | 45 秒 | 3.3 倍 |
| 修改了 package.json | 2 分 30 秒 | 1 分 50 秒 | 1.4 倍 |
| 首次构建 / 缓存失效 | 2 分 30 秒 | 2 分 30 秒 | 无提速 |
进阶 :如果你用 GitHub Container Registry,还可以用
registry类型缓存,把缓存层直接推到镜像仓库里:
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
六、多环境部署策略
真实项目通常有 dev / staging / prod 三套环境。下面展示如何用一套工作流覆盖多环境:
# 在工作流顶部添加环境判断
jobs:
build-and-push:
# ...(同上面的构建 Job)
deploy-dev:
needs: build-and-push
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to DEV
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEV_SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/my-app
# dev 环境用 dev 标签
sed -i 's|image:.*|image: your-dockerhub-username/my-app:dev|' docker-compose.yml
docker compose pull && docker compose up -d
deploy-prod:
needs: build-and-push
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: production # ← GitHub 环境保护:可以配置审批人
steps:
- name: Deploy to PRODUCTION
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/my-app
VERSION=${GITHUB_REF_NAME#v}
sed -i "s|image:.*|image: your-dockerhub-username/my-app:${VERSION}|" docker-compose.yml
docker compose pull && docker compose up -d
关键设计:
-
deploy-dev:push 到 main 自动部署,用dev标签 -
deploy-prod:只有打 tag(如v1.0.0)才触发,用版本号标签 -
environment: production:可以在 GitHub 上配置审批人,防止误操作
七、工作流运行效果
配置完成后,每次推代码到 main 分支,GitHub Actions 会自动触发。在仓库的 Actions 标签页可以看到运行记录:
✅ Docker Build and Deploy #87 pushed 2 minutes ago
├── ✅ build-and-push completed in 1m 23s
│ ├── ✅ Checkout code 5s
│ ├── ✅ Set up Docker Buildx 8s
│ ├── ✅ Login to Docker Hub 2s
│ ├── ✅ Login to GHCR 2s
│ ├── ✅ Generate image tags 1s
│ └── ✅ Build and push 1m 05s
│
└── ✅ deploy completed in 32s
└── ✅ Deploy to server via SSH 30s
推送镜像到 Docker Hub 后的效果:
your-dockerhub-username/my-app
├── latest ← 最新正式版(打 tag 时更新)
├── 1.2.3 ← 版本号标签
├── dev ← 开发版(push 到 main 时更新)
├── a1b2c3d ← Git 短哈希标签(精确到提交)
└── buildcache ← 构建缓存(不用于运行,仅加速构建)
八、其他 CI/CD 工具等效方案
如果你不用 GitHub Actions,下面是 Jenkins 和 GitLab CI 的等效配置概要。
8.1 GitLab CI
GitLab CI 的配置文件是项目根目录的 .gitlab-ci.yml:
# .gitlab-ci.yml
stages:
- build
- deploy
build:
stage: build
image: docker:24
services:
- docker:24-dind # Docker in Docker
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
only:
- main
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
script:
- ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "cd /opt/my-app && docker compose pull && docker compose up -d"
only:
- main
needs: ["build"]
8.2 Jenkins Pipeline
Jenkins 用 Jenkinsfile 定义流水线:
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_IMAGE = 'your-dockerhub-username/my-app'
DOCKERHUB_CREDENTIALS = credentials('dockerhub-credentials')
}
stages {
stage('Build') {
steps {
sh "docker build -t ${DOCKER_IMAGE}:${env.BUILD_NUMBER} ."
}
}
stage('Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials',
usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh "echo ${DOCKER_PASS} | docker login -u ${DOCKER_USER} --password-stdin"
sh "docker push ${DOCKER_IMAGE}:${env.BUILD_NUMBER}"
}
}
}
stage('Deploy') {
steps {
sshagent(['server-ssh-key']) {
sh """
ssh -o StrictHostKeyChecking=no root@your-server \
'cd /opt/my-app && docker compose pull && docker compose up -d'
"""
}
}
}
}
}
三者对比:
| 特性 | GitHub Actions | GitLab CI | Jenkins |
|---|---|---|---|
| 配置文件 | .github/workflows/*.yml |
.gitlab-ci.yml |
Jenkinsfile |
| 运行环境 | GitHub 提供 Runner | GitLab 提供 / 自建 | 自建服务器 |
| 免费额度 | 公开仓库无限,私有仓库 2000 分钟/月 | 公开仓库无限,私有仓库 400 分钟/月 | 完全自建,无限制 |
| 生态插件 | GitHub Marketplace(几千个 Action) | GitLab 内置 + 社区 | 插件生态最丰富(1800+) |
| 上手难度 | ⭐ 简单 | ⭐⭐ 中等 | ⭐⭐⭐ 较复杂 |
九、常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
denied: requested access to the resource is denied |
Docker Hub Token 权限不足 | 重新生成 Token,权限选 Read/Write |
buildx failed with: executor failed running |
Dockerfile 里有命令执行失败 | 查看构建日志,通常是 npm install 或 apt-get 报错 |
| SSH 连接超时 | 服务器安全组未开放 22 端口 | 云服务器控制台放行 22 端口 |
docker compose pull 拉取超时 |
服务器网络到 Docker Hub 慢 | 配置镜像加速(参考本系列第 2 篇) |
| 磁盘空间不足 | 旧镜像堆积 | 定期运行 docker image prune -a 或用 autoprune |
| Actions 不触发 | YAML 文件路径或格式错误 | 确认文件在 .github/workflows/ 目录下,YAML 语法正确 |
十、本文要点回顾
| 要点 | 总结 |
|---|---|
| CI/CD 是什么 | 代码提交后自动构建、测试、部署的全流程自动化 |
| GitHub Actions 核心概念 | Workflow → Event → Job → Step → Action |
| 工作流文件 | .github/workflows/docker-build.yml,定义了从构建到部署的完整流程 |
| 镜像标签策略 | dev 环境用 dev + Git SHA,生产环境用版本号 + latest |
| 缓存优化 | cache-from: type=gha 利用 GitHub 缓存,构建速度提升 3 倍+ |
| 敏感信息 | 全部放 GitHub Secrets,永远不要硬编码在代码里 |
| 多环境部署 | 用 if 条件 + environment 保护,区分 dev / prod |
| 等效工具 | GitLab CI(.gitlab-ci.yml)、Jenkins(Jenkinsfile),原理相同 |
CI/CD 的本质是"把人的操作变成机器的流程"。 Docker 在其中扮演"标准化打包"的角色------同一份 Dockerfile 构建出的镜像,在任何环境都一模一样,这就是 CI/CD 能可靠运行的基石。
下集预告
到这里,Docker 的日常使用和自动化部署都讲完了。下一篇我们进入底层原理篇------Docker 到底是怎么做到"隔离"的?Namespace、Cgroup、UnionFS 这三大核心技术,将揭开容器"看起来像虚拟机,其实不是"的秘密。理解这些原理,你在排查容器问题时就不再是"黑盒调试"了。