Jenkins Pipeline 中实现 Docker in Docker 及前端构建与镜像清理实践
一、 Jenkins 容器化部署与 Docker 权限映射
要在 Jenkins 容器内部执行 Docker 命令(即 Docker outside of Docker / Docker in Docker 模式),需要将宿主机的 Docker 守护进程套接字和二进制文件挂载到容器中,并确保容器内的用户具有相应的执行权限。
1. 启动 Jenkins 容器
通过挂载 /var/run/docker.sock 和 Docker 可执行文件,使容器能够与宿主机的 Docker 引擎通信。
docker run -d \
--name jenkins-server \
--privileged \
-p 8080:8080 \
-p 50000:50000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
-v /opt/jenkins_home:/var/jenkins_home \
-v /etc/localtime:/etc/localtime:ro \
jenkins/jenkins:lts-jdk17
2. 修复 Docker 组权限
挂载套接字后,容器内的 jenkins 用户可能没有权限访问 /var/run/docker.sock。需要使容器内的 Docker 组 GID 与宿主机保持一致。
# 在宿主机上查看 docker 组的 GID
grep docker /etc/group
# 进入 Jenkins 容器并切换到 root 用户
docker exec -it -u root jenkins-server bash
# 假设宿主机的 docker 组 GID 为 998,在容器内创建同名同 GID 的组
groupadd -g 998 docker-host
usermod -aG docker-host jenkins
# 切换到 jenkins 用户并验证
su - jenkins
docker ps
二、 Pipeline 中的 Docker Agent 配置模式
Jenkins Pipeline 支持在全局或特定 Stage 中动态拉起 Docker 容器作为执行环境。
1. 全局 Agent 模式
整个 Pipeline 运行在指定的 Docker 镜像中,适合单一技术栈的项目。
pipeline {
agent {
docker {
image 'maven:3.8.6-eclipse-temurin-11'
args '-v /opt/jenkins_home/.m2:/root/.m2 -w /workspace'
}
}
stages {
stage('Compile Project') {
steps {
sh 'mvn clean compile -DskipTests'
}
}
}
}
2. Stage 级别 Agent 模式
为不同的构建阶段指定不同的 Docker 镜像,适合多语言或前后端分离的微服务项目。
pipeline {
agent none
stages {
stage('Backend Compilation') {
agent {
docker {
image 'gradle:7.5-jdk17'
args '-v /opt/jenkins_home/.gradle:/home/gradle/.gradle'
}
}
steps {
sh 'gradle build -x test'
}
}
stage('Frontend Asset Build') {
agent {
docker {
image 'node:18-alpine'
args '-v /opt/jenkins_home/.npm-cache:/root/.npm'
}
}
steps {
sh 'npm ci && npm run build'
}
}
}
}
三、 前端项目自动化构建实践
1. 准备前端工程代码
在实际生产环境中,代码通常从 Git 仓库拉取。此处以标准的 Node.js 项目结构为例,确保工作空间包含 package.json 及源码目录。
# 克隆代码到 Jenkins 工作空间
git clone https://github.com/example/frontend-app.git /opt/jenkins_home/workspace/frontend-app
cd /opt/jenkins_home/workspace/frontend-app
ls -la
2. 声明式 Pipeline 构建前端
使用声明式语法配置 Node.js 环境,并处理 npm 的缓存和权限问题。
pipeline {
agent none
stages {
stage('Build Web Assets') {
agent {
docker {
image 'node:16.20-alpine'
// 使用 root 用户运行以避免权限拒绝,挂载 npm 缓存目录
args '-u 0:0 -v /opt/jenkins_home/.npm-cache:/root/.npm'
}
}
steps {
sh """
npm config set cache /root/.npm
npm config set registry https://registry.npmmirror.com
cd frontend-app
npm install --no-audit --no-fund
npm run build
ls -lh dist/
"""
}
}
}
}
3. 脚本式 Pipeline 构建前端
利用 docker.image().inside() 方法实现更细粒度的控制。
pipeline {
agent { node { label "linux-node" } }
stages {
stage('Compile Frontend') {
steps {
script {
def nodeImage = docker.image('node:16.20-alpine')
nodeImage.inside('-u 0:0 -v /opt/jenkins_home/.npm-cache:/root/.npm') {
sh """
npm config set cache /root/.npm
npm config set registry https://registry.npmmirror.com
cd frontend-app
npm ci
npm run build
"""
}
}
}
}
}
}
四、 前端构建常见痛点与优化方案
- 权限拒绝问题:在 Alpine 等精简版 Node 镜像中,非 root 用户执行
npm install可能会遇到EACCES错误。通过在args中传递-u 0:0强制以 root 身份运行容器进程可有效解决。 - 依赖下载缓慢:
- 持久化缓存:通过
-v参数将宿主机的目录映射到容器内的/root/.npm,避免每次构建都重新下载依赖。 - 配置国内镜像源:在构建脚本中动态设置
npm config set registry https://registry.npmmirror.com以加速包拉取。
- 持久化缓存:通过
五、 Harbor 镜像仓库标签自动化清理
随着构建次数增加,Harbor 仓库中会堆积大量无用的镜像 Tag。以下脚本通过调用 Harbor REST API (v2.0) 自动清理指定项目的历史标签。注意:此操作不可逆,请务必在测试环境验证后再应用于生产环境。
@Library('shared-pipeline-library@main') _
def harborUtils = new com.devops.HarborManager()
def repoName = "${params.REPO_NAME}"
def appName = "${params.APP_NAME}"
def targetTag = "${params.TAG_NAME}"
currentBuild.description = "Cleanup: ${appName} / ${targetTag}"
pipeline {
agent { label 'build-agent' }
stages {
stage('Fetch Existing Tags') {
steps {
timeout(time: 3, unit: 'MINUTES') {
script {
harborUtils.log("Querying artifacts for ${appName}...", 'INFO')
try {
def apiResponse = httpRequest(
authentication: 'harbor-creds-id',
url: "https://harbor.internal.local/api/v2.0/projects/${repoName}/repositories/${appName}/artifacts?page_size=50",
ignoreSslErrors: true
)
def artifacts = readJSON text: apiResponse.content
// 提取所有 tag 名称
env.TAG_LIST = artifacts.collect { it.tags?.name }.flatten().findAll { it != null }.join(',')
} catch (Exception e) {
harborUtils.log("Failed to fetch artifacts: ${e.message}", 'ERROR')
env.TAG_LIST = ""
}
}
}
}
}
stage('Purge Old Tags') {
steps {
timeout(time: 10, unit: 'MINUTES') {
script {
def tagsToDelete = env.TAG_LIST ? env.TAG_LIST.split(',') : []
harborUtils.log("Found ${tagsToDelete.size()} tags to process.", 'INFO')
tagsToDelete.eachWithIndex { tag, index ->
if (tag != targetTag) { // 保留当前目标 Tag
harborUtils.log("[${index + 1}/${tagsToDelete.size()}] Deleting artifact: ${tag}", 'WARN')
httpRequest(
httpMode: 'DELETE',
authentication: 'harbor-creds-id',
url: "https://harbor.internal.local/api/v2.0/projects/${repoName}/repositories/${appName}/artifacts/${tag}",
ignoreSslErrors: true
)
sleep 2 // 避免请求频率过高触发限流
}
}
}
}
}
}
}
post {
always {
cleanWs()
}
}
}
