强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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/hadolint-action@v3.1.0
        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 Actionsdocker/build-push-action 配合 Buildx 缓存构建
GitLab CIDinD 或 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 常见问题的排查方法。