跳到主要内容
预计阅读 32 分钟

让代码自己飞上线 —— CI/CD流水线与Docker的深度集成

如果说Docker解决了”环境一致性”问题,那CI/CD解决的就是”交付自动化”问题。当两者结合在一起,你就拥有了一条从代码提交到生产部署的全自动高速公路。

📋 开篇自测:你已经知道多少?

  1. CI和CD分别代表什么?它们解决的核心问题是什么?
  2. 在CI/CD流水线中,Docker镜像通常在哪个阶段被构建?
  3. 什么是”镜像标签策略”?为什么不能在生产环境用latest标签?

一、从手动部署到自动化流水线

让我描述一个没有CI/CD的团队的日常:

周五下午五点,产品经理说:“这个功能必须今天上线。“开发小王把代码写完、测试通过(在自己电脑上),然后开始部署:

  1. SSH登录到服务器
  2. 执行 git pull 拉最新代码
  3. 发现服务器上Node.js版本不对,开始升级
  4. 升级完发现某个包编译失败,开始装编译工具
  5. 折腾了两个小时终于跑起来了
  6. 测试发现有Bug,回滚——但忘了之前改了哪些配置
  7. 最后小王在公司加班到凌晨

这个场景是不是似曾相识?问题出在哪里?手动部署流程太脆弱、太依赖个人经验、太容易出错。

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最佳实践清单

  1. 测试先行:镜像构建一定要在测试通过之后
  2. 确定性标签:生产环境不用latest,用commit hash或语义版本号
  3. 安全扫描:在推送到仓库前扫描安全漏洞
  4. 最小权限:CI/CD系统只赋予必要的权限
  5. 机密管理:密码、密钥等通过CI平台的Secret管理,不写在代码或配置文件中
  6. 构建缓存:充分利用Docker层缓存和CI平台缓存加速构建
  7. 生产手动确认:测试环境可以全自动,生产部署保留人工确认环节
  8. 回滚方案:确保任何时候都能快速回滚到上一个稳定版本
  9. 通知机制:构建失败或部署完成时通知相关人员
  10. 清理旧镜像:定期清理不再使用的旧版本镜像,节省存储空间

📝 掌握度自测

  1. 解释CI和CD分别解决什么问题。在Docker化的项目中,Docker镜像在CI/CD流程的哪个环节被构建?

  2. 为什么生产环境不应该使用latest标签?请举一个可能出问题的具体场景。

  3. 在GitHub Actions中,如何让CI环境中的测试代码能够连接到PostgreSQL和Redis?

  4. 什么是蓝绿部署?它解决了什么问题?简述其基本原理。

  5. 你的CI流水线中,构建Docker镜像特别慢。请列举至少三种加速策略。

💡 自我评估

  • 全部答对:你已经具备了搭建Docker化CI/CD流水线的完整知识。恭喜,最后一章我们来看更大的舞台——容器编排。
  • 答对3-4题:对CI/CD与Docker的集成理解得不错。建议选一个自己的项目实际配置一条流水线来练练手。
  • 答对1-2题:CI/CD是一个实践性很强的领域。建议先在GitHub上创建一个简单项目,配一个最基础的GitHub Actions工作流(只做构建和测试),逐步增加复杂度。

购买课程解锁全部内容

告别「在我电脑上能跑」:Docker 容器化实战

¥29.90