第14章:Docker 与 CI/CD
本章目标:掌握 Docker 在 CI/CD 流水线中的应用,实现代码提交到自动部署的全流程自动化。
14.1 CI/CD 概念
14.1.1 什么是 CI/CD
┌─────────────────────────────────────────────────────────────┐
│ CI/CD 流水线 │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │提交 │ → │构建 │ → │测试 │ → │打包 │ → │部署 │ │
│ │Code │ │Build│ │Test │ │Image│ │Deploy│ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ CI: 持续集成 (Continuous Integration) │
│ 代码提交后自动构建和测试 │
│ │
│ CD: 持续交付/部署 (Continuous Delivery/Deployment) │
│ 通过测试后自动打包和部署 │
└─────────────────────────────────────────────────────────────┘
14.1.2 CI/CD 的价值
| 价值 | 说明 |
|---|---|
| 快速反馈 | 代码提交后立即知道是否破坏了功能 |
| 减少风险 | 小批量频繁发布,降低每次发布风险 |
| 自动化 | 减少人工操作,避免人为错误 |
| 可追溯 | 每次发布都有完整的构建和部署记录 |
| 一致性 | 开发、测试、生产环境完全一致 |
14.2 GitLab CI/CD 集成
14.2.1 GitLab CI/CD 架构
┌─────────────────────────────────────────────────────────────┐
│ GitLab CI/CD 架构 │
│ │
│ ┌──────────────┐ │
│ │ GitLab │ │
│ │ Server │ │
│ │ (.gitlab-ci.yml) │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ GitLab │ │ GitLab │ │
│ │ Runner 1 │ │ Runner 2 │ (CI/CD 执行器) │
│ │ (Docker) │ │ (Docker) │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Docker │ │ 部署目标 │ │
│ │ Registry │ │ (服务器) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
14.2.2 .gitlab-ci.yml 配置
yaml
# .gitlab-ci.yml
stages:
- build
- test
- security
- package
- deploy
variables:
DOCKER_REGISTRY: registry.example.com
IMAGE_NAME: ${DOCKER_REGISTRY}/myproject/app
IMAGE_TAG: ${CI_COMMIT_SHORT_SHA}
# 构建阶段
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $DOCKER_USER -p $DOCKER_PASSWORD $DOCKER_REGISTRY
script:
- docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
- docker push ${IMAGE_NAME}:${IMAGE_TAG}
only:
- main
- develop
# 测试阶段
test:
stage: test
image: ${IMAGE_NAME}:${IMAGE_TAG}
services:
- mysql:8.0
- redis:7
variables:
MYSQL_ROOT_PASSWORD: testpassword
MYSQL_DATABASE: testdb
DATABASE_URL: mysql2://root:testpassword@mysql:3306/testdb
REDIS_URL: redis://redis:6379
script:
- npm install
- npm run test
- npm run test:integration
only:
- main
- develop
- merge_requests
# 安全扫描
security_scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL ${IMAGE_NAME}:${IMAGE_TAG}
allow_failure: true
only:
- main
# 打包镜像(添加更多标签)
package:
stage: package
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $DOCKER_USER -p $DOCKER_PASSWORD $DOCKER_REGISTRY
script:
- docker pull ${IMAGE_NAME}:${IMAGE_TAG}
- docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
- docker push ${IMAGE_NAME}:latest
only:
- main
# 部署到测试环境
deploy_staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
script:
- ssh $STAGING_USER@$STAGING_HOST "
docker pull ${IMAGE_NAME}:${IMAGE_TAG} &&
docker compose -f docker-compose.staging.yml up -d"
environment:
name: staging
url: https://staging.example.com
only:
- develop
# 部署到生产环境
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
script:
- ssh $PRODUCTION_USER@$PRODUCTION_HOST "
docker pull ${IMAGE_NAME}:${IMAGE_TAG} &&
docker compose -f docker-compose.prod.yml up -d"
environment:
name: production
url: https://example.com
when: manual # 手动触发
only:
- main
14.3 GitHub Actions 集成
14.3.1 GitHub Actions 工作流
yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 构建和测试
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- 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=sha,prefix=
- name: Build and push Docker image
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: Run tests
run: |
docker compose -f docker-compose.test.yml up --abort-on-container-exit
docker compose -f docker-compose.test.yml down
# 安全扫描
security:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
# 部署到测试环境
deploy-staging:
needs: security
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment:
name: staging
url: https://staging.example.com
steps:
- name: Deploy to staging
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker compose -f docker-compose.staging.yml up -d
# 部署到生产环境
deploy-production:
needs: security
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://example.com
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker compose -f docker-compose.prod.yml up -d
14.4 Jenkins Pipeline 集成
14.4.1 Jenkins Docker Pipeline
groovy
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
IMAGE_NAME = "${DOCKER_REGISTRY}/myproject/app"
IMAGE_TAG = "${env.BUILD_NUMBER}"
}
stages {
stage('Build') {
steps {
script {
docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
}
}
}
stage('Test') {
steps {
script {
docker.image("${IMAGE_NAME}:${IMAGE_TAG}").inside {
sh 'npm install'
sh 'npm test'
}
}
}
}
stage('Security Scan') {
steps {
script {
sh """
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest \
image --exit-code 1 --severity HIGH,CRITICAL \
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
stage('Push') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
docker.image("${IMAGE_NAME}:${IMAGE_TAG}").push()
docker.image("${IMAGE_NAME}:${IMAGE_TAG}").push('latest')
}
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
script {
sshagent(['staging-ssh-key']) {
sh """
ssh ${STAGING_USER}@${STAGING_HOST} "
docker pull ${IMAGE_NAME}:${IMAGE_TAG} &&
docker compose -f docker-compose.staging.yml up -d
"
"""
}
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "Deploy to production?"
ok "Yes, deploy it!"
}
steps {
script {
sshagent(['production-ssh-key']) {
sh """
ssh ${PRODUCTION_USER}@${PRODUCTION_HOST} "
docker pull ${IMAGE_NAME}:${IMAGE_TAG} &&
docker compose -f docker-compose.prod.yml up -d
"
"""
}
}
}
}
}
post {
always {
sh "docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true"
}
success {
slackSend(
color: 'good',
message: "Build ${env.BUILD_NUMBER} succeeded"
)
}
failure {
slackSend(
color: 'danger',
message: "Build ${env.BUILD_NUMBER} failed"
)
}
}
}
14.5 多阶段构建优化 CI/CD
14.5.1 优化 Dockerfile
dockerfile
# 优化的多阶段 Dockerfile
# Stage 1: 依赖安装
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: 构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: 测试
FROM builder AS tester
RUN npm run test
# Stage 4: 生产
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
# 安全加固
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
14.5.2 CI/CD 中的构建优化
yaml
# .github/workflows/optimized.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build with cache
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# 利用 GitHub Actions 缓存加速构建
14.6 Docker Compose 在 CI/CD 中的应用
14.6.1 测试环境编排
yaml
# docker-compose.test.yml
version: '3.8'
services:
app:
build:
context: .
target: tester
environment:
- DATABASE_URL=postgresql://testuser:testpass@db:5432/testdb
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
command: npm run test
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
14.6.2 集成测试命令
bash
# 在 CI/CD 中运行集成测试
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from app
# 参数说明:
# --build 构建镜像
# --abort-on-container-exit 任一容器退出则停止所有容器
# --exit-code-from app 使用 app 容器的退出码作为最终退出码
14.7 镜像版本管理策略
14.7.1 语义化版本
bash
# 语义化版本格式
MAJOR.MINOR.PATCH
# 示例
v1.0.0 初始版本
v1.1.0 新增功能
v1.1.1 Bug 修复
v2.0.0 重大变更
# 在 CI/CD 中自动打标签
# GitLab CI
deploy_production:
script:
- docker tag ${IMAGE_NAME}:${CI_COMMIT_SHA} ${IMAGE_NAME}:v${CI_COMMIT_TAG}
- docker push ${IMAGE_NAME}:v${CI_COMMIT_TAG}
only:
- tags
14.7.2 镜像标签策略
bash
# 推荐的标签策略
myapp:v1.2.3 语义化版本
myapp:commit-a1b2c3d Git SHA
myapp:2024.06.15 日期标签
myapp:latest 最新版本(仅开发环境)
myapp:staging 环境标签
myapp:production 环境标签
14.8 动手实验
实验 14.1:GitLab CI/CD
bash
# 1. 创建 GitLab 项目
# 2. 添加 .gitlab-ci.yml
# 3. 配置 CI/CD 变量
# DOCKER_USER
# DOCKER_PASSWORD
# STAGING_HOST
# STAGING_USER
# SSH_PRIVATE_KEY
# 4. 推送代码触发流水线
git push origin main
# 5. 在 GitLab 中查看流水线状态
实验 14.2:GitHub Actions
bash
# 1. 创建 GitHub 项目
# 2. 添加 .github/workflows/ci-cd.yml
# 3. 配置 Secrets
# STAGING_HOST
# STAGING_USER
# STAGING_SSH_KEY
# 4. 推送代码触发工作流
git push origin main
# 5. 在 GitHub 中查看 Actions 状态
实验 14.3:Jenkins Pipeline
bash
# 1. 安装 Jenkins
docker run -d --name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins-data:/var/jenkins_home \
jenkins/jenkins:lts
# 2. 访问 http://localhost:8080
# 3. 安装 Docker Pipeline 插件
# 4. 创建 Pipeline 项目
# 5. 配置 Jenkinsfile
# 6. 运行构建
14.9 本章小结
| CI/CD 工具 | 特点 | 适用场景 |
|---|---|---|
| GitLab CI/CD | 与 GitLab 深度集成 | GitLab 用户 |
| GitHub Actions | 与 GitHub 深度集成 | GitHub 用户 |
| Jenkins | 功能强大、插件丰富 | 复杂流水线 |
| Docker Compose | 多容器编排 | 测试环境 |
14.10 课后练习
- 基础题:使用 GitHub Actions 或 GitLab CI/CD 构建和推送 Docker 镜像。
- 进阶题:配置完整的 CI/CD 流水线(构建→测试→安全扫描→部署)。
- 实践题:使用 Jenkins 构建企业级 CI/CD 流水线。
📖 下一章:故障排查与面试题 ------ 常见问题处理和面试准备