Docker CI/CD 要解决什么问题

前面的 Docker 系列已经介绍了 Dockerfile、Docker Compose、镜像构建、镜像 Push 以及 Harbor 私有镜像仓库。

如果每次发布都手动执行:

git pull

docker build -t registry.example.com/backend/api:v1.0.0 .

docker push registry.example.com/backend/api:v1.0.0

ssh server

docker pull registry.example.com/backend/api:v1.0.0

docker compose up -d

项目规模较小时问题不大,但随着服务数量增加,手工发布会逐渐暴露出几个问题:

  • 容易漏执行步骤。

  • 镜像 Tag 容易写错。

  • 测试和生产环境操作不一致。

  • 发布过程缺少统一记录。

  • 无法方便地回滚。

  • 开发人员需要直接登录生产服务器。

  • 发布效率随着项目数量增加而下降。

CI/CD 的核心目标,就是把这些重复操作交给自动化系统完成。

一个典型的 Docker CI/CD 流程如下:

Git Push
   │
   ▼
CI Pipeline
   │
   ├── Checkout
   ├── Test
   ├── Build Image
   ├── Tag Image
   └── Push Image
           │
           ▼
        Harbor
           │
           ▼
        Deploy
           │
           ▼
      Production

最终开发人员只需要:

git push

后面的构建、镜像发布和部署由 CI/CD 系统自动完成。


CI、CD 和 Docker 的关系

CI/CD 并不是 Docker 专属概念。

CI 指 Continuous Integration,即持续集成,核心是:

代码提交
  ↓
自动构建
  ↓
自动测试
  ↓
发现问题

CD 通常包含 Continuous Delivery 和 Continuous Deployment 两种实践。

Continuous Delivery 强调:

代码
 ↓
构建
 ↓
测试
 ↓
镜像
 ↓
随时可以发布

Continuous Deployment 则进一步自动执行:

代码
 ↓
构建
 ↓
测试
 ↓
Push
 ↓
Deploy

Docker 在这个过程中主要负责标准化应用交付物:

Source Code
     ↓
Dockerfile
     ↓
Docker Image
     ↓
Container

因此,Docker CI/CD 可以理解为:

Git
 ↓
CI/CD
 ↓
Docker Build
 ↓
Docker Registry
 ↓
Docker Deploy

Harbor 则承担中间的镜像存储和分发职责。


一个完整的 Docker CI/CD 架构

假设有一个 PHP API 服务:

GitHub
   │
   │ git push
   ▼
GitHub Actions
   │
   ├── Checkout
   ├── PHP Test
   ├── Docker Build
   └── Docker Push
          │
          ▼
      Harbor
          │
          │ docker pull
          ▼
   Production Server
          │
          ▼
    Docker Compose
          │
          ▼
       PHP API

各组件的职责比较明确:

组件作用
GitHub保存源代码
GitHub Actions执行 CI/CD
Dockerfile定义镜像构建过程
Docker Buildx构建 Docker 镜像
Harbor保存和分发镜像
Docker Compose管理生产容器
Production Server运行应用

这种架构非常适合中小型团队,也适合 PHP、Node.js、Go、Java 等后端项目。

Docker 官方已经提供针对 GitHub Actions 的完整 CI/CD 集成方案,包括镜像构建、登录 Registry、Buildx、缓存、多平台构建和镜像元数据等能力。


GitHub Actions 工作流

GitHub Actions 的工作流文件通常放在:

.github/
└── workflows/
    └── docker.yml

一个最基础的工作流:

name: Docker CI/CD

on:
  push:
    branches:
      - main

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Build
        uses: docker/build-push-action@v7
        with:
          context: .
          push: false
          tags: my-api:latest

这个 Workflow 在 main 分支发生 Push 时执行。

执行过程:

git push
   ↓
GitHub
   ↓
GitHub Actions
   ↓
Checkout
   ↓
Setup Buildx
   ↓
Docker Build

这里暂时没有 Push 到 Registry。


添加 Docker 镜像测试

生产环境不应该直接:

Git Push
 ↓
Docker Build
 ↓
Docker Push

更合理的流程是:

Git Push
 ↓
代码检查
 ↓
单元测试
 ↓
Docker Build
 ↓
镜像检查
 ↓
Docker Push

例如 PHP 项目可以先执行:

- name: Install dependencies
  run: composer install --no-interaction --prefer-dist

- name: Run tests
  run: vendor/bin/phpunit

如果测试失败:

PHPUnit
  │
  └── Failed
       │
       ▼
    Workflow Failed
       │
       X
     不 Push

只有测试成功后才继续构建镜像。

因此,一个更合理的 CI Pipeline 是:

Checkout
   ↓
Install
   ↓
Lint
   ↓
Test
   ↓
Docker Build
   ↓
Docker Push

Docker 镜像 Tag 设计

CI/CD 中一个非常重要的问题是:

每次构建的镜像到底应该叫什么?

不建议所有构建都使用:

latest

例如:

registry.example.com/backend/api:latest

因为 latest 无法准确表达当前运行的是哪个版本。

更推荐使用 Git Commit SHA:

registry.example.com/backend/api:a8f52d7

或者 Git Tag:

registry.example.com/backend/api:v1.2.0

实际项目可以同时生成多个 Tag:

registry.example.com/backend/api:v1.2.0
registry.example.com/backend/api:a8f52d7

其中:

v1.2.0

用于人类识别版本。

a8f52d7

用于精确定位源代码。

Docker 官方提供 docker/metadata-action 用于根据 Git Reference 和 GitHub Events 自动生成镜像 Tag、Label 等元数据。

例如:

- name: Docker metadata
  id: meta
  uses: docker/metadata-action@v6
  with:
    images: registry.example.com/backend/api
    tags: |
      type=sha
      type=ref,event=tag      

这样可以避免在 Shell 中手动拼接复杂的 Tag。


将镜像 Push 到 Harbor

前面的文章已经搭建了 Harbor:

registry.example.com

假设项目:

backend

镜像:

api

最终镜像地址:

registry.example.com/backend/api

首先需要在 GitHub Repository 中配置 Harbor 凭证。

建议配置:

HARBOR_USERNAME
HARBOR_TOKEN

其中:

HARBOR_USERNAME

使用 Harbor Robot Account。

HARBOR_TOKEN

保存 Robot Account Token。

不要把真实密码直接写进 Workflow。

然后:

- name: Login to Harbor
  uses: docker/login-action@v4
  with:
    registry: registry.example.com
    username: ${{ secrets.HARBOR_USERNAME }}
    password: ${{ secrets.HARBOR_TOKEN }}

Docker 官方提供 docker/login-action 用于登录 Docker Hub 和其他 Registry。

接下来构建并 Push:

- name: Build and Push
  uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: |
            registry.example.com/backend/api:${{ github.sha }}

这里的:

push: true

表示构建成功后直接将镜像推送到 Registry。


一个完整的 GitHub Actions CI

先实现 CI,不急着自动部署。

name: Docker CI

on:
  push:
    branches:
      - main

  pull_request:
    branches:
      - main

env:
  REGISTRY: registry.example.com
  IMAGE_NAME: backend/api

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, pdo, pdo_mysql
          coverage: none

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run tests
        run: vendor/bin/phpunit

  docker:
    runs-on: ubuntu-latest
    needs:
      - test

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Buildx
        uses: docker/setup-buildx-action@v4

      - name: Login to Harbor
        if: github.event_name == 'push'
        uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.HARBOR_USERNAME }}
          password: ${{ secrets.HARBOR_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=ref,event=tag            

      - name: Build and Push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

这里有一个重要设计:

push: ${{ github.event_name == 'push' }}

因此:

Pull Request
    ↓
Test
    ↓
Build
    ↓
不 Push

而:

Push main
    ↓
Test
    ↓
Build
    ↓
Push Harbor

这样可以避免每次 Pull Request 都向正式镜像仓库写入镜像。

Docker 官方也提供了在 GitHub Actions 中使用 Buildx、缓存、Metadata 和 Build Push Action 的标准实践。


从 CI 进入 CD

到这里已经完成:

Git Push
 ↓
Test
 ↓
Docker Build
 ↓
Docker Push
 ↓
Harbor

但是服务器还没有更新。

CD 的任务就是:

Harbor
 ↓
Production Server
 ↓
docker pull
 ↓
docker compose up

例如生产服务器已经存在:

/opt/my-api/
├── compose.yaml
└── .env

compose.yaml:

services:
  api:
    image: registry.example.com/backend/api:${IMAGE_TAG}

    restart: always

    ports:
      - "8080:80"

    env_file:
      - .env

生产服务器中的:

.env

保存:

IMAGE_TAG=a8f52d7

部署时只需要修改:

IMAGE_TAG=新的Commit SHA

然后:

docker compose pull

docker compose up -d

这样就完成一次部署。


使用 SSH 自动部署

最简单的 CD 实现方式是 GitHub Actions 通过 SSH 登录生产服务器。

Pipeline:

GitHub Actions
      │
      │ SSH
      ▼
Production Server
      │
      ├── docker login
      ├── docker pull
      └── docker compose up -d

GitHub Secrets:

DEPLOY_HOST
DEPLOY_USER
DEPLOY_SSH_KEY

Workflow:

- name: Deploy
  uses: appleboy/ssh-action@v1
  with:
    host: ${{ secrets.DEPLOY_HOST }}
    username: ${{ secrets.DEPLOY_USER }}
    key: ${{ secrets.DEPLOY_SSH_KEY }}
    script: |
      cd /opt/my-api

      export IMAGE_TAG=${{ github.sha }}

      docker compose pull

      docker compose up -d

      docker image prune -f      

这种方式简单直接,非常适合中小型项目。

但是生产环境需要特别注意:

GitHub Actions
      │
      │ SSH
      ▼
生产服务器

意味着 CI 系统拥有生产服务器访问权限。

因此应该使用:

  • 独立部署用户。

  • 独立 SSH Key。

  • 最小服务器权限。

  • 禁止使用 root。

  • 限制 SSH 来源。

  • 对部署操作进行审计。


完整的 Docker CI/CD Workflow

把前面的 CI 和 CD 合并起来:

name: Docker CI/CD

on:
  push:
    branches:
      - main

env:
  REGISTRY: registry.example.com
  IMAGE_NAME: backend/api

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, pdo, pdo_mysql
          coverage: none

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run tests
        run: vendor/bin/phpunit

  build:
    name: Build and Push
    runs-on: ubuntu-latest
    needs:
      - test

    outputs:
      image: ${{ steps.meta.outputs.tags }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Buildx
        uses: docker/setup-buildx-action@v4

      - name: Login to Harbor
        uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.HARBOR_USERNAME }}
          password: ${{ secrets.HARBOR_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
                        type=sha

      - name: Build and Push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs:
      - build

    steps:
      - name: Deploy to Production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/my-api

            export IMAGE_TAG=${{ github.sha }}

            docker compose pull

            docker compose up -d

            docker image prune -f            

最终流程:

                GitHub
                   │
                git push
                   │
                   ▼
              ┌─────────┐
              │   Test  │
              └────┬────┘
                   │
                   ▼
              ┌─────────┐
              │  Build  │
              └────┬────┘
                   │
                   ▼
              ┌─────────┐
              │  Push   │
              └────┬────┘
                   │
                   ▼
                Harbor
                   │
                   │ SSH
                   ▼
            Production Server
                   │
              docker pull
                   │
                   ▼
            docker compose
                   │
                   ▼
                Running

这就是一个完整的 Docker CI/CD Pipeline。


镜像缓存与构建速度

CI/CD 中 Docker Build 最大的问题之一就是构建速度。

如果每次执行:

docker build .

都从头下载依赖,会浪费大量时间。

BuildKit 支持缓存,GitHub Actions 也可以作为 Build Cache Backend。

例如:

- name: Build and Push
  uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

这样可以让后续构建复用之前的缓存。

Docker 官方 GitHub Actions 文档也提供了 GitHub Actions Cache Backend 的配置方式。

Dockerfile 本身也需要配合缓存设计。

例如:

COPY composer.json composer.lock ./

RUN composer install \
    --no-dev \
    --no-interaction \
    --prefer-dist

COPY . .

不要:

COPY . .

RUN composer install

因为源代码变化会导致 Composer 依赖安装这一层失去缓存。


多平台镜像构建

如果服务器同时存在:

x86_64
ARM64

可以构建多平台镜像:

- name: Setup QEMU
  uses: docker/setup-qemu-action@v4

- name: Setup Buildx
  uses: docker/setup-buildx-action@v4

- name: Build and Push
  uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    platforms: linux/amd64,linux/arm64
    tags: ${{ steps.meta.outputs.tags }}

最终:

registry.example.com/backend/api:v1.0.0
                    │
        ┌───────────┴───────────┐
        ▼                       ▼
    linux/amd64              linux/arm64

用户执行:

docker pull registry.example.com/backend/api:v1.0.0

Docker 会根据当前机器的平台选择对应镜像。

Docker 官方 GitHub Actions 支持 QEMU、Buildx 和多平台镜像构建。


生产环境中的发布策略

真正的生产环境不建议简单地:

main
 ↓
自动部署生产

更合理的方式是区分环境:

Pull Request
    ↓
CI
    ↓
Test
    ↓
Build
    ↓
Push

main
    ↓
Build
    ↓
Push Harbor
    ↓
Deploy Staging

Git Tag
    ↓
Build
    ↓
Push Harbor
    ↓
Deploy Production

例如:

v1.0.0

表示正式发布。

Pipeline:

git tag v1.0.0
       │
       ▼
GitHub Actions
       │
       ├── Test
       ├── Build
       ├── Push
       │
       ▼
Harbor
       │
       ▼
Production

这样生产环境发布具备明确的版本边界。


回滚应该怎么做

Docker CI/CD 最大的优势之一就是回滚简单。

假设当前:

v1.2.0

出现问题。

之前版本:

v1.1.0

仍然存在于 Harbor:

registry.example.com/backend/api:v1.1.0
registry.example.com/backend/api:v1.2.0

只需要:

export IMAGE_TAG=v1.1.0

docker compose pull

docker compose up -d

即可恢复。

因此,生产环境不要随意删除历史镜像。

建议:

保留最近 N 个版本

或者:

保留最近 N 天镜像

同时结合 Harbor 的镜像保留策略进行清理。


Docker CI/CD 的安全实践

CI/CD 系统通常同时拥有:

源代码权限
Registry 权限
服务器权限

因此必须重点保护 Secrets。

不要:

env:
  HARBOR_PASSWORD: "123456"

应该:

password: ${{ secrets.HARBOR_TOKEN }}

生产环境建议至少做到:

GitHub
 │
 ├── Repository Secret
 │
 ├── Harbor Robot Account
 │
 └── Deploy SSH Key

权限应该遵循最小权限原则:

CI
 │
 ├── Harbor
 │     └── Push backend/api
 │
 └── Production
       └── Deploy User

而不是:

CI
 │
 └── root
       └── 全部服务器权限

此外,现代 Docker Build 流程还可以生成 SBOM 和 provenance attestations,用于记录镜像的软件组成以及构建来源。Docker 官方的 build-push-action 支持 sbom 和 provenance 配置。

例如:

- name: Build and Push
  uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    sbom: true
    provenance: mode=max

对于对供应链安全要求较高的生产环境,可以进一步加入镜像扫描、SBOM、签名和部署前策略检查。


从 Git Push 到 Deploy 的最终模型

整个 Docker 系列到这里,可以形成一个比较完整的容器化交付体系:

                       Git
                        │
                    git push
                        │
                        ▼
                 ┌─────────────┐
                 │ CI Pipeline │
                 └──────┬──────┘
                        │
                 ┌──────▼──────┐
                 │    Test     │
                 └──────┬──────┘
                        │
                 ┌──────▼──────┐
                 │ Docker Build│
                 └──────┬──────┘
                        │
                 ┌──────▼──────┐
                 │ Docker Push │
                 └──────┬──────┘
                        │
                        ▼
                     Harbor
                        │
                   Docker Pull
                        │
                        ▼
                Production Server
                        │
                Docker Compose
                        │
                        ▼
                    Container

如果进一步扩展,可以变成:

Git
 │
 ▼
CI
 │
 ├── Test
 ├── Lint
 ├── Build
 ├── Scan
 ├── SBOM
 └── Push
       │
       ▼
    Harbor
       │
       ├── Image
       ├── Tag
       ├── Scan
       └── Retention
              │
              ▼
             CD
              │
       ┌──────┴──────┐
       ▼             ▼
   Staging       Production
       │             │
       ▼             ▼
    Docker        Docker
    Compose       Compose

这已经从单纯的「Docker 镜像发布」演变成了一套完整的容器化软件交付体系。

总结

Docker CI/CD 的核心并不是某一个工具,而是把软件交付过程标准化:

代码
 ↓
测试
 ↓
构建
 ↓
镜像
 ↓
Registry
 ↓
部署

结合前面的 Docker、Dockerfile、Docker Compose 和 Harbor,可以形成:

Dockerfile
    ↓
Docker Build
    ↓
Harbor
    ↓
Docker Compose

再加入 GitHub Actions:

Git Push
    ↓
GitHub Actions
    ↓
Test
    ↓
Docker Build
    ↓
Docker Push
    ↓
Harbor
    ↓
Production Deploy

对于中小型项目,这套架构已经能够满足大多数实际生产需求。

后续如果项目继续扩大,再逐步引入 Kubernetes、GitOps、Argo CD、镜像签名、供应链安全、灰度发布和自动回滚等能力,而不是一开始就引入过多基础设施。

Docker 官方目前也提供了从 GitHub Actions 构建、缓存、多平台构建到 SBOM 和 provenance 的完整能力,可以在现有 Pipeline 基础上逐步增强。