Docker CI/CD 集成实战:GitLab CI 与 Jenkins 流水线最佳实践
系统讲解 Docker 在 CI/CD 流水线中的最佳实践:GitLab CI 完整流水线(构建-扫描-推送-部署四阶段)、Jenkins Pipeline with Docker Agent、DinD(Docker-in-Docker)vs Socket 挂载的安全对比、镜像 tag 策略(commit SHA/语义版本/环境标签)、并行测试与缓存优化,以及 GitLab CI + Harbor + Swarm/K8s 的端到端 CD 实现。
Docker 与 CI/CD 的结合是企业容器化落地的核心环节。本文以 GitLab CI 为主、Jenkins 为辅,讲解从代码提交到生产部署的完整自动化链路,重点关注安全性、构建速度和镜像管理策略。
CI/CD 流水线总体设计
代码提交(git push)
↓
CI 流水线:
阶段1 lint & test → 代码检查 + 单元测试
阶段2 build → docker buildx 构建镜像 + 缓存
阶段3 scan → Trivy 安全扫描(高危漏洞阻断)
阶段4 push → 推送到 Harbor 私有仓库
↓(仅 main 分支)
CD 流水线:
阶段5 deploy-staging → 部署到测试环境
阶段6 integration-test → 集成测试
阶段7 deploy-prod → 手动审批后部署到生产
一、GitLab CI 完整流水线
基础配置
# .gitlab-ci.yml
variables:
# 镜像仓库
REGISTRY: harbor.company.com
IMAGE_NAME: $REGISTRY/backend/$CI_PROJECT_NAME
# 镜像 Tag 策略
# feature 分支:用 commit SHA(可追溯,不会覆盖)
# main 分支:用 SHA + latest
# tag 推送:用 git tag + SHA
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
# BuildKit 缓存
CACHE_IMAGE: $REGISTRY/cache/$CI_PROJECT_NAME:buildcache
# Docker 配置
DOCKER_TLS_CERTDIR: "" # DinD 配置(见下文)
DOCKER_DRIVER: overlay2
stages:
- test
- build
- scan
- push
- deploy
# 默认配置(所有 job 继承)
default:
interruptible: true # 新 pipeline 触发时中断旧的
tags:
- docker # 只在有 docker tag 的 runner 上运行
测试阶段
# 单元测试(并行)
unit-test:
stage: test
image: python:3.11-slim
cache:
key: pip-$CI_COMMIT_REF_SLUG
paths:
- .pip-cache/
before_script:
- pip install --cache-dir .pip-cache -r requirements-dev.txt
script:
- pytest tests/unit/ -v --tb=short --junitxml=junit.xml
artifacts:
reports:
junit: junit.xml
when: always
expire_in: 1 week
# 代码质量检查
lint:
stage: test
image: python:3.11-slim
script:
- pip install flake8 mypy
- flake8 src/
- mypy src/
allow_failure: false
构建阶段(BuildKit + 注册表缓存)
build:
stage: build
image: docker:27
services:
- name: docker:27-dind
alias: docker
variables:
DOCKER_TLS_CERTDIR: ""
before_script:
- docker login -u "$HARBOR_USER" -p "$HARBOR_TOKEN" $REGISTRY
- docker buildx create --use --name ci-builder --driver docker-container
script:
# 构建并利用注册表缓存
- |
docker buildx build \
--platform linux/amd64 \
--cache-from type=registry,ref=${CACHE_IMAGE} \
--cache-to type=registry,ref=${CACHE_IMAGE},mode=max \
--build-arg APP_VERSION=${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg GIT_COMMIT=${CI_COMMIT_SHA} \
-t ${IMAGE_NAME}:${IMAGE_TAG} \
--output type=docker,dest=/tmp/image.tar \
.
# 保存镜像供后续 job 使用
- mv /tmp/image.tar image.tar
artifacts:
paths:
- image.tar
expire_in: 1 day
安全扫描阶段
trivy-scan:
stage: scan
image: aquasec/trivy:latest
before_script:
- docker load < image.tar
script:
# 扫描镜像,高危/严重漏洞时 exit code 非0(自动失败)
- |
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format sarif \
--output trivy-results.sarif \
${IMAGE_NAME}:${IMAGE_TAG}
artifacts:
reports:
sast: trivy-results.sarif # GitLab Security Dashboard 展示
when: always
expire_in: 1 week
allow_failure: false # 高危漏洞必须修复才能继续
needs:
- job: build
artifacts: true
推送阶段
push:
stage: push
image: docker:27
services:
- docker:27-dind
before_script:
- docker login -u "$HARBOR_USER" -p "$HARBOR_TOKEN" $REGISTRY
- docker load < image.tar
script:
# feature 分支:只推 SHA tag
- docker push ${IMAGE_NAME}:${IMAGE_TAG}
# main 分支:额外推 latest
- |
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
docker push ${IMAGE_NAME}:latest
fi
# git tag:额外推语义版本 tag
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${CI_COMMIT_TAG}
docker push ${IMAGE_NAME}:${CI_COMMIT_TAG}
fi
needs:
- job: trivy-scan
- job: build
artifacts: true
only:
- main
- tags
- /^release\/.*$/
部署阶段
deploy-staging:
stage: deploy
image: alpine:3.19
before_script:
- apk add --no-cache openssh-client curl
- eval $(ssh-agent -s)
- echo "$DEPLOY_KEY" | ssh-add -
script:
# 通过 SSH 触发 Swarm 更新
- |
ssh -o StrictHostKeyChecking=no deploy@swarm-manager-01 \
"docker service update \
--image ${IMAGE_NAME}:${IMAGE_TAG} \
--with-registry-auth \
backend_api"
# 等待更新完成
- sleep 30
# 验证部署
- |
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://api-staging.company.com/health)
if [ "$HTTP_STATUS" != "200" ]; then
echo "❌ 部署验证失败(HTTP $HTTP_STATUS),触发回滚"
ssh deploy@swarm-manager-01 "docker service rollback backend_api"
exit 1
fi
echo "✅ staging 部署成功"
environment:
name: staging
url: https://api-staging.company.com
only:
- main
needs:
- push
deploy-production:
stage: deploy
image: alpine:3.19
script:
- |
ssh deploy@swarm-manager-01 \
"APP_VERSION=${IMAGE_TAG} docker stack deploy \
-c /opt/stacks/myapp/stack.yml \
--with-registry-auth \
myapp"
environment:
name: production
url: https://api.company.com
when: manual # 手动触发(需要人工审批)
only:
- main
needs:
- deploy-staging
二、Jenkins Pipeline
// Jenkinsfile
pipeline {
agent none // 使用动态 agent
environment {
REGISTRY = 'harbor.company.com'
IMAGE_NAME = "${REGISTRY}/backend/${env.JOB_NAME}"
IMAGE_TAG = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
CACHE_IMAGE = "${REGISTRY}/cache/${env.JOB_NAME}:buildcache"
}
stages {
stage('Test') {
agent {
docker {
image 'python:3.11-slim'
args '-v /var/cache/pip:/root/.cache/pip'
}
}
steps {
sh 'pip install -r requirements-dev.txt'
sh 'pytest tests/ --junitxml=test-results.xml'
}
post {
always {
junit 'test-results.xml'
}
}
}
stage('Build & Scan') {
agent {
label 'docker-buildx' // 有 buildx 的节点
}
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-credentials',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_TOKEN'
)]) {
sh """
docker login -u \$HARBOR_USER -p \$HARBOR_TOKEN ${REGISTRY}
docker buildx build \\
--cache-from type=registry,ref=${CACHE_IMAGE} \\
--cache-to type=registry,ref=${CACHE_IMAGE},mode=max \\
--build-arg APP_VERSION=${IMAGE_TAG} \\
-t ${IMAGE_NAME}:${IMAGE_TAG} \\
--load \\
.
trivy image \\
--exit-code 1 \\
--severity HIGH,CRITICAL \\
--ignore-unfixed \\
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
stage('Push') {
agent { label 'docker-buildx' }
when {
branch 'main'
}
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-credentials',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_TOKEN'
)]) {
sh """
docker login -u \$HARBOR_USER -p \$HARBOR_TOKEN ${REGISTRY}
docker push ${IMAGE_NAME}:${IMAGE_TAG}
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
docker push ${IMAGE_NAME}:latest
"""
}
}
}
stage('Deploy Staging') {
agent { label 'deploy' }
when { branch 'main' }
steps {
sshagent(['deploy-key']) {
sh """
ssh -o StrictHostKeyChecking=no deploy@swarm-manager-01 \\
docker service update --image ${IMAGE_NAME}:${IMAGE_TAG} \\
--with-registry-auth backend_api
"""
}
}
}
stage('Deploy Production') {
agent { label 'deploy' }
when { branch 'main' }
input {
message "确认部署到生产环境?"
ok "部署"
submitter "ops-team"
}
steps {
sshagent(['deploy-key']) {
sh """
ssh deploy@swarm-manager-01 \\
"APP_VERSION=${IMAGE_TAG} docker stack deploy \\
-c /opt/stacks/myapp/stack.yml \\
--with-registry-auth myapp"
"""
}
}
}
}
post {
failure {
slackSend(
channel: '#ops-alerts',
color: 'danger',
message: "❌ 构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|查看>)"
)
}
success {
slackSend(
channel: '#ops-deploys',
color: 'good',
message: "✅ 部署成功: ${IMAGE_NAME}:${IMAGE_TAG}"
)
}
}
}
三、DinD vs Socket 挂载
在 CI 中运行 Docker 命令有两种方式:
方式1:Docker-in-Docker(DinD)
CI 容器内运行独立 Docker daemon
优点:完全隔离,安全
缺点:需要特权模式(--privileged),性能差(嵌套虚拟化)
方式2:挂载宿主机 Docker Socket(-v /var/run/docker.sock:/var/run/docker.sock)
CI 容器直接使用宿主机的 Docker daemon
优点:共享宿主机缓存,构建速度快
缺点:安全风险(CI 容器拥有宿主机 Docker 的完整控制权)
方式3:Docker Socket Proxy(推荐折中方案)
只暴露需要的 API(如只允许 build/push)
# GitLab CI — DinD 方式(安全但慢)
services:
- name: docker:27-dind
alias: docker
variables:
DOCKER_TLS_CERTDIR: "/certs"
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_CERT_PATH: "/certs/client"
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: 1
# GitLab Runner 配置 — Socket 挂载方式(推荐自托管 runner)
# /etc/gitlab-runner/config.toml
[[runners]]
name = "docker-runner"
executor = "docker"
[runners.docker]
image = "docker:27"
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
# 不需要 privileged = true
四、镜像 Tag 策略
推荐 Tag 规范:
开发分支(feature/*):
image:abc1234 ← commit SHA,用于调试
不推 latest(避免污染)
主分支(main):
image:abc1234 ← commit SHA
image:latest ← 最新主分支
image:main-20260515 ← 日期(便于回溯)
发布 Tag(v1.2.3):
image:abc1234 ← commit SHA
image:v1.2.3 ← 语义版本
image:v1.2 ← 次版本
image:v1 ← 主版本
image:stable ← 稳定版别名
环境 Tag(用于快速标识当前环境版本):
image:staging ← 当前 staging 环境的版本
image:production ← 当前 production 环境的版本
(每次部署时更新这个 tag)
小结
Docker CI/CD 的三个关键实践:
构建缓存:--cache-from/to type=registry 让每次 CI 构建复用上次的层,代码不变时 build job 从几分钟缩短到几十秒。
安全扫描前置:Trivy 扫描放在 push 之前,高危漏洞直接阻断流水线,确保进入仓库的镜像都通过了基本安全检查。
精准 Tag:用 commit SHA 作为主 Tag(可追溯),用语义版本和环境 Tag(staging/production)作为别名,任何时候都能精确定位线上运行的版本。
