Docker 完全指南 / 16 - CI/CD 集成
16 - CI/CD 集成
将 Docker 集成到 CI/CD 流水线中,实现自动化构建、测试和部署。
16.1 CI/CD 与 Docker 概述
典型的 Docker CI/CD 流水线:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 代码 │───>│ 构建 │───>│ 测试 │───>│ 推送 │───>│ 部署 │
│ 提交 │ │ 镜像 │ │ 容器 │ │ 仓库 │ │ 环境 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
git push docker build docker run docker push docker compose
/ kubectl apply
CI/CD 工具对比
| 工具 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| GitHub Actions | 云服务 | 原生 GitHub 集成 | GitHub 项目 |
| GitLab CI | 云/自建 | 完整 DevOps 平台 | GitLab 项目 |
| Jenkins | 自建 | 插件丰富、高度自定义 | 复杂企业环境 |
| CircleCI | 云服务 | 快速配置、并行执行 | 中小项目 |
| Drone | 自建 | Docker 原生、轻量 | Docker 生态 |
16.2 GitHub Actions
基本概念
| 概念 | 说明 |
|---|---|
| Workflow | .github/workflows/*.yml 文件定义的自动化流程 |
| Job | 一组 Step,在同一个 Runner 上执行 |
| Step | 单个操作(运行命令或 Action) |
| Action | 可复用的操作单元 |
| Runner | 执行 Job 的机器(GitHub 托管或自托管) |
| Secret | 加密存储的敏感变量 |
示例:构建并推送 Docker 镜像
# .github/workflows/docker-build.yml
name: Build and Push Docker Image
on:
push:
branches: [main, develop]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
# 1. 检出代码
- name: Checkout
uses: actions/checkout@v4
# 2. 设置 Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 3. 登录容器仓库
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 4. 生成镜像标签
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
# 5. 构建并推送
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
多架构构建
name: Multi-Architecture Build
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
myuser/myapp:latest
myuser/myapp:${{ github.ref_name }}
cache-from: type=registry,ref=myuser/myapp:buildcache
cache-to: type=registry,ref=myuser/myapp:buildcache,mode=max
完整 CI/CD 流水线
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# ---- 代码检查 ----
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint Dockerfile
uses: hadolint/[email protected]
with:
dockerfile: Dockerfile
# ---- 单元测试 ----
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Build test image
run: docker build -t myapp:test --target test .
- name: Run tests
run: |
docker run --rm \
--network host \
-e DB_HOST=localhost \
-e DB_PASSWORD=test \
myapp:test npm test
# ---- 安全扫描 ----
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:scan .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:scan
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
# ---- 构建并推送 ----
build-and-push:
needs: [lint, test, security]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myuser/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ---- 部署 ----
deploy:
needs: build-and-push
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /opt/myapp
docker compose pull
docker compose up -d
16.3 GitLab CI
.gitlab-ci.yml 基本结构
# .gitlab-ci.yml
stages:
- build
- test
- security
- deploy
variables:
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# ---- 构建阶段 ----
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_NAME .
- docker tag $IMAGE_NAME $CI_REGISTRY_IMAGE:latest
- docker push $IMAGE_NAME
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# ---- 测试阶段 ----
test:
stage: test
image: $IMAGE_NAME
services:
- postgres:16
variables:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
DB_HOST: postgres
script:
- npm install
- npm test
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# ---- 安全扫描 ----
trivy-scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity CRITICAL $IMAGE_NAME
allow_failure: true
rules:
- if: $CI_COMMIT_BRANCH == "main"
# ---- 部署到开发环境 ----
deploy-dev:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
script:
- ssh $DEV_USER@$DEV_HOST "cd /opt/app && docker compose pull && docker compose up -d"
environment:
name: development
url: https://dev.example.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
# ---- 部署到生产环境 ----
deploy-prod:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
script:
- ssh $PROD_USER@$PROD_HOST "cd /opt/app && docker compose pull && docker compose up -d"
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
GitLab CI 高级配置
# 使用 Docker Buildx 缓存
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
docker buildx create --use
docker buildx build \
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache \
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max \
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \
--tag $CI_REGISTRY_IMAGE:latest \
--push \
.
# 使用 GitLab Container Registry 镜像清理
# 项目 → Settings → Packages and registries → Clean up policies
16.4 Docker-in-Docker (DinD)
DinD 方案对比
| 方案 | 说明 | 优势 | 劣势 |
|---|---|---|---|
| DinD | 在容器中运行 Docker daemon | 完全隔离 | 需要特权模式 |
| DooD | 共享宿主机 Docker daemon | 简单、性能好 | 安全性较低 |
| Kaniko | 无 daemon 构建镜像 | 安全、无需特权 | 仅构建,不运行 |
| Buildah | 无 daemon 构建工具 | rootless、安全 | 需要安装 |
DinD 配置
# GitLab CI DinD
build:
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "/certs/client"
script:
- docker build -t myapp .
- docker push myapp:latest
Kaniko 方案(无需 Docker daemon)
# GitLab CI with Kaniko
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.20.0-debug
entrypoint: [""]
script:
- |
/kaniko/executor \
--context $CI_PROJECT_DIR \
--dockerfile $CI_PROJECT_DIR/Dockerfile \
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \
--destination $CI_REGISTRY_IMAGE:latest \
--cache=true
16.5 测试策略
Docker 容器化测试
# GitHub Actions: 使用容器运行测试
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20-alpine
services:
redis:
image: redis:7-alpine
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
env:
REDIS_HOST: redis
DB_HOST: postgres
DB_PASSWORD: test
- name: Run integration tests
run: npm run test:integration
- name: Run E2E tests
run: |
docker compose -f docker-compose.test.yml up -d
npm run test:e2e
docker compose -f docker-compose.test.yml down
测试 Docker Compose 文件
# docker-compose.test.yml
services:
app-under-test:
build:
context: .
target: test
environment:
- NODE_ENV=test
- DB_HOST=test-db
- REDIS_HOST=test-redis
depends_on:
test-db:
condition: service_healthy
test-redis:
condition: service_healthy
test-db:
image: postgres:16
environment:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
retries: 10
test-redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
retries: 10
16.6 部署策略
SSH 部署
# GitHub Actions: SSH 部署
deploy:
runs-on: ubuntu-latest
needs: build-and-push
environment: production
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
# 拉取最新镜像
docker compose pull
# 滚动更新
docker compose up -d --remove-orphans
# 清理旧镜像
docker image prune -f
# 健康检查
sleep 10
curl -f http://localhost:8080/health || exit 1
Docker Context 远程部署
# 创建远程 Docker context
docker context create remote \
--docker "host=ssh://user@remote-host"
# 使用远程 context
docker --context remote compose up -d
# 切换 context
docker context use remote
镜像标签策略
# 语义化版本标签
tags: |
type=semver,pattern={{version}} # v1.0.0
type=semver,pattern={{major}}.{{minor}} # v1.0
type=semver,pattern={{major}} # v1
type=sha # sha-abc1234
# 分支标签
tags: |
type=ref,event=branch # main, develop
type=ref,event=pr # pr-42
# 自动生成标签
tags: |
type=raw,value={{date 'YYYYMMDD'}} # 20240101
type=raw,value=latest # latest
16.7 安全最佳实践
Secret 管理
# ❌ 不要硬编码密钥
env:
API_KEY: "sk-1234567890"
# ✅ 使用 CI/CD 平台的 Secret 管理
env:
API_KEY: ${{ secrets.API_KEY }}
# ✅ 使用环境变量文件
- name: Create env file
run: |
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
echo "API_KEY=${{ secrets.API_KEY }}" >> .env
# ✅ 使用 Docker Build Secrets
- name: Build with secrets
run: |
docker build \
--secret id=db_password,src=.db_password \
--secret id=api_key,src=.api_key \
-t myapp .
Dockerfile 中使用 Secret
# 使用 BuildKit secrets
RUN --mount=type=secret,id=db_password \
DB_PASSWORD=$(cat /run/secrets/db_password) && \
python setup.py configure --db-password="$DB_PASSWORD"
要点回顾
| 要点 | 核心内容 |
|---|---|
| GitHub Actions | docker/build-push-action 配合 Buildx 缓存构建 |
| GitLab CI | DinD 或 Kaniko 方案,支持自动推送到 Registry |
| 测试策略 | 使用容器化服务运行集成测试 |
| 部署策略 | SSH 部署、Docker Context、Docker Compose |
| 安全 | 使用 CI 平台 Secret,避免硬编码敏感信息 |
注意事项
缓存优化: 使用
cache-from: type=gha(GitHub Actions)或--cache-from type=registry(GitLab)加速构建。
镜像扫描: 在流水线中集成 Trivy 或 Docker Scout 进行漏洞扫描,阻止高危漏洞镜像部署。
最小权限: CI/CD 中使用的 Token/密钥应遵循最小权限原则,仅授予推送镜像所需的权限。
下一步
→ 17 - 故障排查:学习 Docker 常见问题的排查方法。