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 基础上逐步增强。