让代码自己飞上线 —— CI/CD流水线与Docker的深度集成
如果说Docker解决了”环境一致性”问题,那CI/CD解决的就是”交付自动化”问题。当两者结合在一起,你就拥有了一条从代码提交到生产部署的全自动高速公路。
📋 开篇自测:你已经知道多少?
- CI和CD分别代表什么?它们解决的核心问题是什么?
- 在CI/CD流水线中,Docker镜像通常在哪个阶段被构建?
- 什么是”镜像标签策略”?为什么不能在生产环境用latest标签?
一、从手动部署到自动化流水线
让我描述一个没有CI/CD的团队的日常:
周五下午五点,产品经理说:“这个功能必须今天上线。“开发小王把代码写完、测试通过(在自己电脑上),然后开始部署:
- SSH登录到服务器
- 执行
git pull拉最新代码 - 发现服务器上Node.js版本不对,开始升级
- 升级完发现某个包编译失败,开始装编译工具
- 折腾了两个小时终于跑起来了
- 测试发现有Bug,回滚——但忘了之前改了哪些配置
- 最后小王在公司加班到凌晨
这个场景是不是似曾相识?问题出在哪里?手动部署流程太脆弱、太依赖个人经验、太容易出错。
CI/CD的核心思想:把从代码提交到生产部署的每一步都自动化,减少人工干预,提高交付速度和可靠性。
-
CI(Continuous Integration,持续集成):开发者每次提交代码后,自动执行构建、测试,确保新代码不会破坏已有功能。就像工厂流水线上的质检站——每件产品出来都要过一遍质检,不合格的当场打回。
-
CD(Continuous Delivery/Deployment,持续交付/部署):代码通过CI后,自动打包、推送,甚至自动部署到生产环境。就像质检通过后自动装箱发货。
Docker在这个流程中扮演的角色是”标准化的集装箱”——CI阶段把代码和环境一起打包成Docker镜像,CD阶段把这个镜像直接部署到任何环境中,不需要再操心环境配置。
二、Docker + CI/CD的标准工作流
一个典型的Docker化CI/CD流水线长这样:
代码提交 → 触发CI → 运行测试 → 构建镜像 → 推送镜像 → 部署到目标环境
↑ ↓
└─────────────── 如果失败,通知开发者 ←──────────────────────┘
让我展开每个步骤:
步骤一:代码提交 开发者把代码推送到Git仓库(GitHub、GitLab等)。
步骤二:触发CI Git仓库通过Webhook通知CI/CD平台(GitHub Actions、GitLab CI、Jenkins等):“有新代码了,开始干活吧。”
步骤三:运行测试 CI平台拉起一个干净的环境(通常也是Docker容器),安装依赖,运行单元测试、集成测试、代码风格检查。任何一项不通过就停止并通知开发者。
步骤四:构建Docker镜像 测试通过后,按照项目的Dockerfile构建生产镜像。
步骤五:推送镜像 把构建好的镜像推送到镜像仓库(Docker Hub、GitHub Container Registry、或私有仓库)。
步骤六:部署 从镜像仓库拉取最新镜像,在目标环境中启动新容器,替换旧版本。
🤔 想一想 在这个流水线中,如果”运行测试”这一步本身需要数据库、Redis等依赖服务,你会怎么办?(提示:Docker Compose在CI环境中也能用)
三、GitHub Actions实战
GitHub Actions是目前最流行的CI/CD平台之一,与GitHub深度集成,对开源项目免费。让我们用它来构建一条完整的流水线。
基础配置
在项目根目录创建 .github/workflows/ci-cd.yml:
name: CI/CD Pipeline
# 触发条件:推送到main分支 或 提交PR到main分支
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ==================== 第一个任务:测试 ====================
test:
runs-on: ubuntu-latest
services:
# CI环境中启动PostgreSQL服务
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: bookstore_test
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U testuser -d bookstore_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
# CI环境中启动Redis服务
redis:
image: redis:7.2-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: 拉取代码
uses: actions/checkout@v4
- name: 安装Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json
- name: 安装依赖
working-directory: ./backend
run: npm ci
- name: 运行代码风格检查
working-directory: ./backend
run: npm run lint
- name: 运行单元测试
working-directory: ./backend
run: npm test
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: bookstore_test
DB_USER: testuser
DB_PASSWORD: testpass
REDIS_HOST: localhost
- name: 运行集成测试
working-directory: ./backend
run: npm run test:integration
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: bookstore_test
DB_USER: testuser
DB_PASSWORD: testpass
REDIS_HOST: localhost
# ==================== 第二个任务:构建并推送镜像 ====================
build-and-push:
runs-on: ubuntu-latest
needs: test # 依赖test任务成功
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: 拉取代码
uses: actions/checkout@v4
- name: 登录镜像仓库
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 提取镜像元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=raw,value=latest
- name: 设置Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 构建并推送后端镜像
uses: docker/build-push-action@v7
with:
context: ./backend
file: ./backend/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ github.sha }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: 构建并推送前端镜像
uses: docker/build-push-action@v7
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ github.sha }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# ==================== 第三个任务:部署 ====================
deploy:
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: 部署到服务器
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/bookstore
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
逐段解读
触发条件:当代码推送到main分支或者有PR请求时触发。这样每次代码变动都会自动运行流水线。
test任务:注意 services 配置——GitHub Actions原生支持启动Docker服务容器。我们在CI环境中直接跑起了PostgreSQL和Redis,测试代码连接它们就像连接本地服务一样。
build-and-push任务:使用了几个关键的GitHub Actions:
docker/login-action:登录镜像仓库docker/metadata-action:自动生成镜像标签docker/build-push-action:构建并推送镜像,还支持构建缓存(cache-from/cache-to),大幅加速重复构建
deploy任务:通过SSH连接到生产服务器,拉取最新镜像,重启服务。这是最简单的部署方式,适合小团队。
四、镜像标签策略
镜像标签就像软件的版本号,好的标签策略能让你随时知道线上跑的是哪个版本,并且在出问题时快速回滚。
常见的标签策略
Git提交哈希:最精确的方式,每个构建对应唯一的commit。
myapp:a1b2c3d
myapp:e4f5g6h
语义化版本号:符合大众认知习惯。
myapp:1.0.0
myapp:1.0.1
myapp:1.1.0
日期+构建号:简单直观。
myapp:20260312-build42
Git标签:发布时打Git Tag,CI自动用Tag作为镜像标签。
myapp:v1.2.3
推荐的实践
# 每次构建都打两个标签
# 1. commit hash(精确定位)
# 2. latest(方便测试环境快速拉取)
docker build -t myapp:a1b2c3d -t myapp:latest .
docker push myapp:a1b2c3d
docker push myapp:latest
在生产环境中,始终使用确定性标签(commit hash或版本号),绝不使用latest。 这样出问题时你可以精确知道线上跑的是哪个版本的代码,也能快速回滚到上一个好版本。
⚠️ 常见误区
- 误区一:“CI环境里构建的镜像和本地构建的不一样”。只要Dockerfile和构建上下文一致,产出的镜像就是一致的。但要注意基础镜像版本——如果用了latest,CI和本地拉到的可能是不同版本。
- 误区二:“每次都要从头构建镜像很慢”。利用好Docker的层缓存和CI平台的缓存机制,绝大多数构建都能在几分钟内完成。本章示例中的
cache-from: type=gha就是用了GitHub Actions的缓存。- 误区三:“部署就是把镜像推到服务器上运行”。在简单场景下确实如此,但生产环境还需要考虑滚动更新、健康检查、自动回滚、数据库迁移等问题。
五、GitLab CI/CD方案
如果你的团队使用GitLab,配置文件是 .gitlab-ci.yml:
stages:
- test
- build
- deploy
variables:
DOCKER_REGISTRY: registry.gitlab.com
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# 测试阶段
test:
stage: test
image: node:20-alpine
services:
- postgres:16-alpine
- redis:7.2-alpine
variables:
POSTGRES_DB: bookstore_test
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
DB_HOST: postgres
REDIS_HOST: redis
script:
- cd backend
- npm ci
- npm run lint
- npm test
only:
- main
- merge_requests
# 构建镜像
build:
stage: build
image: docker:29
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_TAG -f backend/Dockerfile ./backend
- docker push $IMAGE_TAG
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
# 部署
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts
script:
- ssh $SERVER_USER@$SERVER_HOST "cd /opt/bookstore && docker compose pull && docker compose up -d"
only:
- main
when: manual # 生产部署需要手动确认
注意最后的 when: manual——生产环境的部署需要人工点击确认,这是一个重要的安全门槛。测试环境可以全自动,但生产环境至少要有一个人类审核的环节。
六、高级技巧
使用Docker Compose跑集成测试
在CI中跑集成测试时,可以用Docker Compose拉起完整的服务栈:
# docker-compose.test.yml
services:
test-runner:
build:
context: ./backend
dockerfile: Dockerfile.dev
command: npm run test:integration
environment:
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: bookstore_test
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7.2-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# 在CI中执行
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test-runner
--abort-on-container-exit 表示任何容器退出后停止所有容器,--exit-code-from test-runner 表示用test-runner容器的退出码作为整个命令的退出码(非0表示测试失败)。
镜像安全扫描
在推送镜像之前,可以对镜像进行安全漏洞扫描:
# GitHub Actions中添加安全扫描步骤
# ⚠️ 重要:始终使用 commit SHA 固定 Action 版本,不要使用可变的版本标签(如 @v0.35.0)
# 2026年3月 trivy-action 遭遇严重供应链攻击,76个版本标签中的75个被恶意篡改
# 使用 SHA 可以确保你引用的是经过验证的、不可篡改的代码
- name: 安全扫描
uses: aquasecurity/trivy-action@62c60f210dd4ef5b5e48fba5e5e095cebf254d24 # v0.35.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: 上传扫描结果
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
Trivy会检查镜像中是否包含已知的安全漏洞(CVE)。如果发现高危或严重漏洞,可以阻止部署,强制开发者修复后再上线。
⚠️ GitHub Actions 版本固定最佳实践 上面的示例用 commit SHA(
@62c60f210dd4ef5b5e48fba5e5e095cebf254d24)而非版本标签(@v0.35.0)来引用 Action,这是一个重要的安全实践。版本标签是可变的——维护者(或攻击者)可以随时将标签指向不同的代码。2026年3月 trivy-action 的供应链攻击就是一个真实案例:攻击者篡改了几乎所有版本标签,将其指向恶意代码,窃取CI环境中的密钥。使用 commit SHA 可以彻底避免这类风险。建议对所有第三方 Action 都采用这种固定方式。
零停机部署
最简单的部署方式是”停旧启新”,但这会有几秒到几十秒的服务中断。要实现零停机部署,可以使用蓝绿部署或滚动更新。
一个简易的蓝绿部署脚本:
#!/bin/bash
# deploy.sh
IMAGE=$1
CURRENT=$(docker ps --filter "name=app-blue" --format "{{.Names}}" | head -1)
if [ "$CURRENT" = "app-blue" ]; then
NEW_NAME="app-green"
OLD_NAME="app-blue"
else
NEW_NAME="app-blue"
OLD_NAME="app-green"
fi
# 启动新版本
docker run -d --name $NEW_NAME --network app-net $IMAGE
# 等待新版本就绪
sleep 10
if ! docker exec $NEW_NAME curl -sf http://localhost:4000/health > /dev/null; then
echo "新版本健康检查失败,回滚"
docker rm -f $NEW_NAME
exit 1
fi
# 切换Nginx上游到新版本
# ...(更新Nginx配置并reload)
# 停止旧版本
docker rm -f $OLD_NAME
echo "部署完成:$NEW_NAME"
当然,更成熟的方案是使用Kubernetes的Rolling Update或者Docker Swarm的update策略——这些我们在下一章会介绍。
🤔 想一想 如果你的CI/CD流水线中,构建镜像这一步总是很慢(比如前端项目需要10分钟以上的npm install和build),你有什么办法加速?(提示:Docker层缓存、CI平台缓存、预构建基础镜像)
七、CI/CD最佳实践清单
- 测试先行:镜像构建一定要在测试通过之后
- 确定性标签:生产环境不用latest,用commit hash或语义版本号
- 安全扫描:在推送到仓库前扫描安全漏洞
- 最小权限:CI/CD系统只赋予必要的权限
- 机密管理:密码、密钥等通过CI平台的Secret管理,不写在代码或配置文件中
- 构建缓存:充分利用Docker层缓存和CI平台缓存加速构建
- 生产手动确认:测试环境可以全自动,生产部署保留人工确认环节
- 回滚方案:确保任何时候都能快速回滚到上一个稳定版本
- 通知机制:构建失败或部署完成时通知相关人员
- 清理旧镜像:定期清理不再使用的旧版本镜像,节省存储空间
📝 掌握度自测
-
解释CI和CD分别解决什么问题。在Docker化的项目中,Docker镜像在CI/CD流程的哪个环节被构建?
-
为什么生产环境不应该使用latest标签?请举一个可能出问题的具体场景。
-
在GitHub Actions中,如何让CI环境中的测试代码能够连接到PostgreSQL和Redis?
-
什么是蓝绿部署?它解决了什么问题?简述其基本原理。
-
你的CI流水线中,构建Docker镜像特别慢。请列举至少三种加速策略。
💡 自我评估
- 全部答对:你已经具备了搭建Docker化CI/CD流水线的完整知识。恭喜,最后一章我们来看更大的舞台——容器编排。
- 答对3-4题:对CI/CD与Docker的集成理解得不错。建议选一个自己的项目实际配置一条流水线来练练手。
- 答对1-2题:CI/CD是一个实践性很强的领域。建议先在GitHub上创建一个简单项目,配一个最基础的GitHub Actions工作流(只做构建和测试),逐步增加复杂度。
购买课程解锁全部内容
告别「在我电脑上能跑」:Docker 容器化实战
¥29.90