前言
当构建环境五花八门——有的项目用 Java 17,有的用 Go 1.21,有的需要 Node 20——在 Jenkins Agent 上预装所有环境不现实。Docker 解决了这个问题:每个构建步骤在指定版本的容器中执行,环境完全隔离且可复现。本篇从零搭建一个完整的容器化 CI 流水线。
一、Docker-in-Docker vs Docker Socket 挂载
方式一:挂载 Docker Socket(推荐)
Jenkins Agent (宿主机)
┌──────────────────────────────┐
│ Docker Daemon (宿主机) │
│ /var/run/docker.sock │
│ ↑ 挂载 │
│ ┌────┴───────────────────┐ │
│ │ Pipeline 容器 (maven) │ │
│ │ 执行: docker build ... │ │ ← 通过 socket 调用宿主机 Docker
│ │ 执行: docker push ... │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
优点:简单直接,利用宿主机 Docker 引擎,无需嵌套 Docker
缺点:有安全风险——容器内可以控制宿主机的 Docker
配置方式:
# Dockerfile for Jenkins Agent
FROM maven:3.9-eclipse-temurin-17
# 安装 Docker CLI(只需要 CLI,不需要 daemon)
RUN apt-get update && apt-get install -y docker.io
USER jenkins
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '-v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.m2:/root/.m2'
}
}
stages {
stage('Build') {
steps { sh 'mvn clean package -DskipTests' }
}
stage('Docker Build') {
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
sh 'docker push registry.com/myapp:${BUILD_NUMBER}'
}
}
}
}
方式二:Docker-in-Docker(DinD)
Jenkins Agent (宿主机)
┌──────────────────────────────────┐
│ Pipeline 容器 (maven) │
│ ┌────────────────────────────┐ │
│ │ DinD 容器 (docker:24-dind) │ │ ← 独立 Docker daemon
│ │ 执行: docker build ... │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
优点:完全隔离,不影响宿主机
缺点:性能开销大,需要 --privileged 权限
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '--privileged -v /tmp:/tmp'
}
}
stages {
stage('Start Docker Service') {
steps {
// 启动内部 Docker daemon
sh 'dockerd --host=tcp://0.0.0.0:2375 &'
sh 'sleep 5'
sh 'docker info'
}
}
}
}
**踩坑提示**:Socket 挂载方式有安全风险,因为容器内的用户可以通过 Docker 命令控制宿主机。生产环境建议用 DinD 或专门的 Docker 构建节点。
二、多阶段 Dockerfile 构建
为什么用多阶段构建
传统构建:一个 Dockerfile 全包
→ 镜像包含编译器、源码、依赖 → 镜像 800MB+ → 安全风险
多阶段构建:
→ 阶段1: 编译 (maven 镜像, 含编译工具)
→ 阶段2: 运行 (jre 镜像, 只有运行时) → 镜像 150MB → 安全干净
多阶段 Dockerfile 示例
# === 阶段1: 构建 ===
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
# 先复制 pom.xml 利用缓存层
COPY pom.xml .
RUN mvn dependency:go-offline
# 复制源码并编译
COPY src ./src
RUN mvn clean package -DskipTests
# === 阶段2: 运行 ===
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 从构建阶段复制产物
COPY --from=builder /build/target/*.jar app.jar
# 安全:用非 root 用户运行
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar", "--server.port=8080"]
前端项目多阶段构建
# === 阶段1: 前端构建 ===
FROM node:20-alpine AS frontend-builder
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
# === 阶段2: Nginx 静态服务 ===
FROM nginx:1.25-alpine
COPY --from=frontend-builder /frontend/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost/ || exit 1
三、镜像构建与推送流水线
完整 Pipeline
pipeline {
agent { label 'docker-build' }
environment {
REGISTRY = 'registry.mycompany.com'
IMAGE_NAME = "${REGISTRY}/myapp"
IMAGE_TAG = "${env.BUILD_NUMBER}"
DOFILE_PATH = 'Dockerfile'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build App') {
steps {
// 在 Maven 容器中构建
sh 'mvn clean package -DskipTests'
stash includes: 'target/*.jar', name: 'app-jar'
}
}
stage('Build Image') {
steps {
unstash 'app-jar'
// 多阶段构建
sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} -f ${DOCKERFILE_PATH} ."
// 同时打一个 latest 标签
sh "docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest"
}
}
stage('Security Scan') {
steps {
// 用 Trivy 扫描镜像漏洞
sh """
trivy image --exit-code 1 --severity HIGH,CRITICAL \
--ignorefile .trivyignore \
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
stage('Push') {
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-registry',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh "echo $REG_PASS | docker login $REGISTRY -u $REG_USER --password-stdin"
sh "docker push ${IMAGE_NAME}:${IMAGE_TAG}"
sh "docker push ${IMAGE_NAME}:latest"
}
}
}
stage('Sign Image') {
when {
branch 'main'
}
steps {
// 用 Cosign 签名镜像
sh """
cosign sign --key env://COSIGN_PRIVATE_KEY \
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
stage('Clean') {
steps {
// 清理本地镜像
sh "docker rmi ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest || true"
}
}
}
post {
always {
cleanWs()
}
}
}
镜像标签策略
| 标签 | 用途 | 示例 |
|------|------|------|
| BUILD_NUMBER | 每次构建唯一标识 | 42 |
| GIT_COMMIT | 关联代码版本 | a1b2c3d |
| BRANCH-COMMIT | 分支+commit | main-a1b2c3d |
| latest | 最新稳定版(仅 main 分支) | latest |
| vX.Y.Z | 正式发版 Tag | v1.2.3 |
environment {
GIT_HASH = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
IMAGE_TAG = "${env.BRANCH_NAME}-${GIT_HASH}"
}
四、构建缓存优化
Docker 层缓存
# 好的做法:先复制 lock 文件
COPY package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 坏的做法:每次代码变更都重新安装依赖
COPY . .
RUN npm ci && npm run build
Jenkins 构建缓存
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '-v $HOME/.m2:/root/.m2 -v $HOME/.npm:/root/.npm'
}
}
// 这样 Maven 本地仓库和 npm 缓存会在构建间复用
// 第一次构建下载依赖 5 分钟,后续构建 30 秒
}
BuildKit 缓存
# Dockerfile 中使用 BuildKit 缓存挂载
# syntax=docker/dockerfile:1.6
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests
# 启用 BuildKit
DOCKER_BUILDKIT=1 docker build -t myapp .
**踩坑提示**:Docker 层缓存只在本地有效。在 CI 中每次构建都是新环境,需要通过 Volume 挂载来持久化缓存目录。Docker BuildKit 的 `--mount=type=cache` 是更现代的方案。
五、镜像安全扫描
Trivy 集成
stage('Security Scan') {
steps {
// 安装 Trivy(如果 Agent 上没有)
sh '''
if ! command -v trivy &> /dev/null; then
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
fi
'''
// 扫描镜像,HIGH/CRITICAL 级别漏洞导致构建失败
sh """
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format json \
--output trivy-report.json \
${IMAGE_NAME}:${IMAGE_TAG}
"""
// 归档报告
archiveArtifacts artifacts: 'trivy-report.json', fingerprint: true
// 发布到 Jenkins 报告界面
recordIssues(
tools: [trivy(id: 'trivy', pattern: 'trivy-report.json')],
qualityGates: [[threshold: 1, type: 'TOTAL', unstable: false]]
)
}
}
Cosign 镜像签名
# 生成签名密钥
cosign generate-key-pair
# 设置环境变量:
# COSIGN_PRIVATE_KEY
# COSIGN_PASSWORD
# 签名镜像
cosign sign --key env://COSIGN_PRIVATE_KEY registry.com/myapp:42
# 验证签名(部署前验证)
cosign verify --key cosign.pub registry.com/myapp:42
六、本篇要点回顾
1. Docker Socket 挂载方式简单但有安全风险,生产环境用 DinD 或专用构建节点
2. 多阶段 Dockerfile 构建可以大幅减小镜像体积,移除编译工具和源码
3. 镜像标签用 BRANCH-COMMIT 格式,便于追踪代码版本
4. 构建缓存:Docker 层缓存 + Volume 挂载缓存 + BuildKit 缓存挂载
5. Trivy 扫描 + Cosign 签名实现镜像供应链安全
下一篇预告:CI/CD 的 Jenkins 篇到此结束,接下来进入 GitLab CI 实战篇:《快速入门:.gitlab-ci.yml 语法与 Runner 部署》。

9万+

被折叠的 条评论
为什么被折叠?



