1. 项目概述:为什么今天还在认真搭建 Jenkins?

你点开这篇内容,大概率不是因为对 Jenkins 有情怀,而是被现实按在地上摩擦过——改完一行代码,手动打包、传服务器、解压、重启 Tomcat,等三分钟看日志报错;前端同事改个按钮颜色,要等后端发版窗口;测试环境三天没更新,bug 复现不了;上线前一小时发现配置漏了,手抖输错命令直接把生产库连错了……这些不是段子,是我在 2018 年接手第一个微服务项目时的真实夜班记录。

Jenkins 确实“老”,但它的不可替代性恰恰藏在“不性感”里:它不靠 UI 吸睛,不靠云原生概念讲故事,而是用最笨的办法——把人干的重复动作,变成可追溯、可回滚、可审计、可交接的标准化流水线。尤其当你的团队开始出现“有人离职,部署脚本就失联”“新同事配环境花两天,还配不对”“每次上线都要拉群喊人盯屏”这类信号时,Jenkins 就不是“要不要上”的问题,而是“再不上,系统稳定性已经由运气决定”的临界点。

更关键的是, Jenkins + Docker 的组合,正在成为中小团队技术基建的“事实标准” 。你看热搜词里反复出现的“docker部署springboot项目”“前端vue项目部署”“jenkins自动部署”,背后全是同一套逻辑:用 Docker 镜像固化运行时环境(Java 版本、Node.js 版本、Python 依赖、Nginx 配置),用 Jenkins Pipeline 脚本固化构建部署流程(拉代码→装依赖→跑单元测试→打镜像→推仓库→启容器)。它不追求 Kubernetes 那种抽象层级,而是让每个步骤都看得见、改得了、查得清。我带过的 7 个不同行业项目(从政务系统到跨境电商 SaaS),最终落地的自动化方案,90% 都是从 Jenkins + Docker 起步,再逐步演进。

所以这篇内容不讲“Jenkins 是什么”,而是直接带你走通一条 真实生产环境可用的路径 :从一台干净的 Ubuntu 服务器开始,安装 Jenkins、集成 Docker、配置权限、编写可复用的 Pipeline 脚本、处理 Java/Python/前端项目的差异化部署、解决镜像体积大、构建慢、权限报错等高频坑。所有操作基于 2024 年最新 LTS 版本(Jenkins 2.440.4 + Docker 24.0.7),所有命令可复制粘贴,所有配置项都解释清楚“为什么这么设”。如果你刚配好 Jenkins 却卡在“构建时报 Permission denied”、或者写完 Pipeline 却发现“镜像推不到私有仓库”,那接下来的内容,就是为你写的。

2. 环境准备与核心架构设计

2.1 为什么必须用 Docker 运行 Jenkins?

很多人第一步就栽在安装方式上:直接 apt install jenkins ,然后在宿主机上跑。这看似简单,但埋下三个致命隐患:

  • 环境污染 :Jenkins 插件会往系统 /var/lib/jenkins 写大量缓存和插件包,升级 Jenkins 时极易和系统 Java 版本冲突;
  • 权限地狱 :Jenkins 进程默认以 jenkins 用户运行,而 Docker 守护进程需要 docker 组权限,硬加用户到 docker 组会导致安全策略失效(比如 CI 任务能随意删宿主机容器);
  • 扩展性归零 :未来想加一个 SonarQube 代码扫描节点?得在宿主机再装一套 Java 环境,版本还不能和 Jenkins 冲突。

正确姿势是:用 Docker 容器运行 Jenkins 主体,再通过 Docker Socket 挂载让 Jenkins 调用宿主机 Docker 引擎 。这本质是“容器化运维工具”,好处立竿见影:

  • Jenkins 升级 = docker pull jenkins/jenkins:lts + docker-compose up -d ,5 秒完成,旧数据全在挂载卷里;
  • 权限隔离清晰:Jenkins 容器只拥有它该有的权限(通过 --group-add docker 显式授权),不会越界操作宿主机;
  • 扩展即加容器:SonarQube、Nexus 私有仓库、GitLab Runner,全用 docker-compose.yml 一键编排。

提示:不要用 docker run -d --name jenkins -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock jenkins/jenkins:lts 这种裸命令。必须用 docker-compose.yml 管理,否则后续加挂载卷、环境变量、健康检查都会失控。

2.2 最小可行架构图(非理论,是实操拓扑)

我们不画虚的“CI/CD 分层架构图”,直接给你生产环境里真正连着线的设备关系:

组件 部署位置 关键作用 我的实际配置
Jenkins Master 一台 4C8G Ubuntu 22.04 云服务器 接收 Git Webhook、调度任务、展示构建日志 容器名 jenkins-master ,挂载 /data/jenkins:/var/jenkins_home
Docker Engine 同一台服务器(Jenkins 宿主机) 构建镜像、运行测试容器、部署应用容器 Docker 24.0.7,启用 --experimental (为 BuildKit 做准备)
Private Registry 同一台服务器(用 registry:2 容器) 存储项目镜像,避免推 Docker Hub 的网络延迟和速率限制 docker run -d -p 5000:5000 -v /data/registry:/var/lib/registry registry:2
Git 仓库 自建 GitLab 或 GitHub 私有仓库 代码源,触发 Jenkins 构建 必须配置 Webhook,URL 为 http://<服务器IP>:8080/project/<项目名>

这个架构的关键在于: 所有组件都在同一台物理机(或同一 VPC 内网) 。为什么?因为 Jenkins 构建时要频繁读写 Docker 镜像层(动辄几百 MB),跨机器传输会把构建时间从 2 分钟拖到 15 分钟。我试过把 Registry 放在阿里云 ACR,结果 SpringBoot 项目构建阶段光 push 镜像就卡 8 分钟,最后还是搬回本地 Registry。

2.3 操作系统与基础依赖确认

别跳过这一步!很多“安装失败”其实源于系统预设。在 Ubuntu 22.04 上执行:

# 1. 确认内核支持 overlay2(Docker 默认存储驱动)
$ uname -r
5.15.0-101-generic  # ✅ 5.15+ 内核原生支持

# 2. 检查 systemd 是否启用 cgroup v2(Docker 24+ 强制要求)
$ cat /proc/1/cgroup | head -1
0::/  # ✅ 如果显示 "0::/" 表示 cgroup v2 已启用;若显示 "1:name=systemd:/..." 则需修改 grub

# 3. 禁用 swap(Docker 官方明确要求,否则启动容器报错)
$ sudo swapoff -a
$ echo '# swap disabled for docker' | sudo tee -a /etc/fstab

注意:如果 cat /proc/1/cgroup 显示的是 1:name=systemd:/ ,说明系统在用 cgroup v1。必须修改 /etc/default/grub ,将 GRUB_CMDLINE_LINUX="" 改为 GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1" ,然后 sudo update-grub && sudo reboot 。这是 Docker 24+ 的硬性门槛,跳过必踩坑。

2.4 Docker 安装与 Jenkins 容器初始化

严格按官方文档来,不用 snap apt 仓库存疑版本:

# 卸载旧版(如有)
sudo apt-get remove docker docker-engine docker.io containerd runc

# 安装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 添加 Docker 官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 添加稳定版仓库
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装 Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 启动并设开机自启
sudo systemctl enable docker
sudo systemctl start docker

# 验证(必须看到 "Hello from Docker!")
sudo docker run hello-world

现在创建 docker-compose.yml

# /data/jenkins/docker-compose.yml
version: '3.8'
services:
  jenkins-master:
    image: jenkins/jenkins:lts-jdk17
    container_name: jenkins-master
    restart: unless-stopped
    ports:
      - "8080:8080"
      - "50000:50000"  # JNLP agent 端口,后续加 slave 节点用
    environment:
      - JAVA_OPTS=-Djenkins.install.runSetupWizard=false -Dhudson.model.DownloadService.noSignatureCheck=true
      - JENKINS_OPTS=--httpKeepAliveTimeout=60000
    volumes:
      - /data/jenkins:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock:ro  # 只读挂载,安全第一
      - /usr/bin/docker:/usr/bin/docker:ro  # 让容器内能调用宿主机 docker CLI
      - /data/registry:/data/registry:ro  # 本地 Registry 挂载,供 Jenkins 推送
    group_add:
      - "docker"  # 关键!让 jenkins 用户加入 docker 组
    networks:
      - jenkins-net

networks:
  jenkins-net:
    driver: bridge

启动:

cd /data/jenkins
sudo docker-compose up -d

实操心得:第一次启动后, sudo docker logs jenkins-master 查看日志,重点找 Jenkins initial setup is required 后面的 12 位管理员密码。这个密码在 /data/jenkins/secrets/initialAdminPassword 文件里,但直接读文件不如看日志快。另外, JAVA_OPTS 中的 -Djenkins.install.runSetupWizard=false 是为了跳过向导,避免 Jenkins 自动装一堆用不到的插件(如 Blue Ocean),我们后面按需装。

3. Jenkins 核心配置与权限体系搭建

3.1 初始化配置:绕过向导,直击生产级设置

访问 http://<服务器IP>:8080 ,输入初始密码后, 立刻选择 “Install suggested plugins” → 点 “Restart Jenkins” 。不要选 “Select plugins to install”,因为建议插件集(24 个)已覆盖 95% 场景:Git、Docker Pipeline、Pipeline Utility Steps、Role-based Authorization Strategy(权限控制)、Email Extension(邮件通知)。

重启后,登录管理员账号,进入 Manage Jenkins → Configure System

  • Jenkins URL :填 http://<服务器IP>:8080 (必须带协议和端口,否则邮件通知链接会错);
  • Global properties → Environment variables :添加 DOCKER_REGISTRY=http://<服务器IP>:5000 ,后续 Pipeline 脚本直接引用;
  • E-mail Notification :SMTP 配置(以腾讯企业邮箱为例):
    • SMTP server: smtp.exmail.qq.com
    • Default user E-mail suffix: @yourcompany.com
    • Test e-mail:填自己邮箱,点 “Test configuration” 验证。

注意:这里不配置 “JDK” 或 “Maven” 全局工具。因为我们要用 Docker 容器提供构建环境(Java 17、Maven 3.9、Node 18),全局工具反而会造成版本混乱。所有构建环境都在 Pipeline 里声明。

3.2 权限模型:为什么 Role-based Strategy 是唯一选择

Jenkins 默认的 “Logged-in users can do anything” 权限模型,在 3 人以上团队就是灾难。曾有个客户,测试人员误点了 “Delete this job”,整个支付系统的部署流水线没了。我们必须用 Role-based Authorization Strategy 插件(已随建议插件安装)。

进入 Manage Jenkins → Manage and Assign Roles

  • Manage Roles :创建三类角色:
    • admin-role :勾选 Overall/Read , Overall/Administer , Job/Build , Job/Cancel , Job/Configure , Job/Delete , Job/Read , Job/Workspace , SCM/Tag , View/Read , View/Configure
    • dev-role :勾选 Job/Build , Job/Cancel , Job/Read , SCM/Tag , View/Read
    • test-role :勾选 Job/Build , Job/Read , View/Read
  • Assign Roles :将管理员账号分配给 admin-role ,开发人员账号分配给 dev-role ,测试人员账号分配给 test-role

关键细节: SCM/Tag 权限允许开发人员在构建成功后自动打 Git Tag(如 v1.2.3 ),这是发布管理的基础。没有这个权限,每次发版都要运维手动打 Tag,效率归零。

3.3 凭据管理:安全存储敏感信息的唯一方式

所有密码、Token、密钥, 绝不能写在 Pipeline 脚本里 。必须用 Jenkins 内置凭据系统:

  • 进入 Credentials → System → Global credentials → Add Credentials
  • Kind :选 Username with password
  • Scope :选 Global (Jenkins, nodes, items, all child items)
  • Username :填 Git 仓库用户名(如 gitlab-ci );
  • Password :填对应 Token(GitLab 生成 Personal Access Token,勾选 read_repository write_repository );
  • ID :填 gitlab-credentials (后续 Pipeline 用这个 ID 引用)。

同样方式添加 Docker Registry 凭据:

  • Kind Username with password
  • Username admin (本地 Registry 默认用户名);
  • Password Harbor12345 (启动 Registry 时指定的密码);
  • ID docker-registry-credentials

实操心得:凭据 ID 必须全小写、无下划线、无特殊字符。我见过因 ID 写成 docker_registry_creds 导致 Pipeline 报 No credentials matching 'docker_registry_creds' found 的案例。Jenkins 凭据系统对 ID 匹配极其严格。

3.4 全局工具配置:只配 Docker,其他交给容器

进入 Manage Jenkins → Global Tool Configuration

  • Docker :点击 “Docker” → “Docker installation” → 勾选 “Install automatically” → 选择 “Docker 24.0.7” → “Install from docker.com”;
  • 其他工具(JDK/Maven/Node)全部留空

为什么?因为 Jenkins 本身只负责调度,真正的构建环境由 Docker 容器提供。比如 Maven 构建,我们用 maven:3.9-amazoncorretto-17 镜像,里面已预装 JDK 17 和 Maven 3.9,版本精准可控。如果在这里配了全局 Maven,Pipeline 里又用容器,两个环境版本不一致,构建结果就不可信。

4. Pipeline 脚本编写与多语言项目部署实战

4.1 Pipeline 基础语法:Declarative vs Scripted,选哪个?

Jenkins Pipeline 有两种写法:

  • Declarative Pipeline :结构固定( pipeline { agent {} stages {} } ),语法严格,适合新手和标准化场景;
  • Scripted Pipeline :基于 Groovy,灵活度高,但易出错,适合复杂逻辑。

强烈推荐 Declarative 。原因很实在:

  • 错误提示友好: stage('Build') 缺少 steps{} ,Jenkins 直接标红报错行;
  • IDE 支持好:VS Code 安装 “Jenkins Pipeline Linter Connector” 插件,写完就能校验语法;
  • 可视化强:Blue Ocean 界面能自动生成流程图,每个 stage 点开看详细日志。

下面所有示例均用 Declarative。

4.2 SpringBoot 项目部署:从代码到容器的完整链路

假设你的项目结构是标准 Maven:

my-springboot-app/
├── pom.xml
├── src/
│   └── main/
│       ├── java/
│       └── resources/
└── Jenkinsfile  # 放在项目根目录

Jenkinsfile 内容:

pipeline {
    agent {
        docker {
            image 'maven:3.9-amazoncorretto-17'
            args '-u root'  // 以 root 运行,避免权限问题
            reuseNode true  // 复用 Jenkins agent 节点,共享工作区
        }
    }

    environment {
        // 从 Jenkins 全局配置读取
        DOCKER_REGISTRY = 'http://<服务器IP>:5000'
        APP_NAME = 'my-springboot-app'
        IMAGE_TAG = "${BUILD_NUMBER}"  // 用构建号做镜像 tag
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm  // 从 Git 拉代码
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'  // 跳过测试,加快构建
                sh 'cp target/*.jar app.jar'  // 复制 jar 到工作区根目录,方便后续 Dockerfile 引用
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    // 构建镜像,tag 为 registry 地址 + 项目名 + 构建号
                    def customImage = docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}")
                    // 推送到本地 Registry
                    docker.withRegistry("${DOCKER_REGISTRY}", 'docker-registry-credentials') {
                        customImage.push()
                    }
                }
            }
        }

        stage('Deploy') {
            steps {
                script {
                    // 在宿主机执行:停止旧容器,启动新容器
                    sh """
                        docker stop ${APP_NAME} || true
                        docker rm ${APP_NAME} || true
                        docker run -d \
                            --name ${APP_NAME} \
                            -p 8081:8080 \
                            -e SPRING_PROFILES_ACTIVE=prod \
                            --restart=always \
                            ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}
                    """
                }
            }
        }
    }

    post {
        success {
            emailext (
                subject: "SUCCESS: ${env.JOB_NAME} [${env.BUILD_NUMBER}]",
                body: """<p>构建成功!</p>
                         <p>项目:${env.JOB_NAME}</p>
                         <p>构建号:${env.BUILD_NUMBER}</p>
                         <p>镜像地址:<a href="${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}">${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}</a></p>""",
                recipientProviders: [[$class: 'DevelopersRecipientProvider']]
            )
        }
        failure {
            emailext (
                subject: "FAILED: ${env.JOB_NAME} [${env.BUILD_NUMBER}]",
                body: """<p>构建失败,请检查日志。</p>
                         <p>构建日志:<a href="${env.BUILD_URL}console">${env.BUILD_URL}console</a></p>""",
                recipientProviders: [[$class: 'DevelopersRecipientProvider']]
            )
        }
    }
}

关键参数解析:

  • reuseNode true :让 Docker 容器和 Jenkins agent 共享同一个工作区( /var/jenkins_home/workspace/<job-name> ),否则 mvn package 生成的 jar 在容器里, docker build 在宿主机找不到;
  • args '-u root' :Maven 镜像默认用户是 root ,但 Jenkins 启动容器时会切到 jenkins 用户,导致 mvn 命令权限不足,强制 -u root 解决;
  • docker.withRegistry(...) :用之前配置的凭据 ID docker-registry-credentials 认证,否则 push 会 401 Unauthorized。

4.3 Python Flask 项目部署:处理虚拟环境与依赖

Flask 项目结构:

my-flask-app/
├── requirements.txt
├── app.py
├── Dockerfile
└── Jenkinsfile

Dockerfile

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Jenkinsfile

pipeline {
    agent any  // 不用 Docker agent,因为要先构建镜像,再推送到 Registry

    environment {
        DOCKER_REGISTRY = 'http://<服务器IP>:5000'
        APP_NAME = 'my-flask-app'
        IMAGE_TAG = "${BUILD_NUMBER}"
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build and Push Docker Image') {
            steps {
                script {
                    // 构建镜像(使用项目根目录的 Dockerfile)
                    def customImage = docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}")
                    // 推送
                    docker.withRegistry("${DOCKER_REGISTRY}", 'docker-registry-credentials') {
                        customImage.push()
                    }
                }
            }
        }

        stage('Deploy') {
            steps {
                script {
                    sh """
                        docker stop ${APP_NAME} || true
                        docker rm ${APP_NAME} || true
                        docker run -d \
                            --name ${APP_NAME} \
                            -p 5000:5000 \
                            --restart=always \
                            ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}
                    """
                }
            }
        }
    }

    post {
        success {
            emailext (
                subject: "SUCCESS: ${env.JOB_NAME} [${env.BUILD_NUMBER}]",
                body: "Flask 应用部署成功!访问 http://<服务器IP>:5000",
                recipientProviders: [[$class: 'DevelopersRecipientProvider']]
            )
        }
    }
}

注意:Python 项目不用在 Pipeline 里装依赖,全部交给 Dockerfile RUN pip install 。这样做的好处是:

  • 依赖版本锁定在镜像层,和 Jenkins 构建环境无关;
  • pip install 的缓存可以复用(Docker 构建时, requirements.txt 不变则跳过安装);
  • 避免 Jenkins 服务器上 Python 环境混乱(比如全局 pip 和 virtualenv 冲突)。

4.4 前端 Vue 项目部署:Nginx 静态资源托管

Vue 项目结构:

my-vue-app/
├── package.json
├── vue.config.js
├── nginx.conf  # 自定义 Nginx 配置
└── Jenkinsfile

nginx.conf

server {
    listen 80;
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }
    location /api/ {
        proxy_pass http://backend-service:8080/;
    }
}

Jenkinsfile

pipeline {
    agent {
        docker {
            image 'node:18-alpine'
            args '-u root'
            reuseNode true
        }
    }

    environment {
        DOCKER_REGISTRY = 'http://<服务器IP>:5000'
        APP_NAME = 'my-vue-app'
        IMAGE_TAG = "${BUILD_NUMBER}"
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh 'npm ci'  // 用 ci 替代 install,更快更可靠
                sh 'npm run build'  // 生成 dist/ 目录
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    // 构建镜像:基础镜像用 nginx,把 dist/ 复制进去
                    def customImage = docker.build(
                        "${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}",
                        "-f Dockerfile -t ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG} ."
                    )
                    docker.withRegistry("${DOCKER_REGISTRY}", 'docker-registry-credentials') {
                        customImage.push()
                    }
                }
            }
        }

        stage('Deploy') {
            steps {
                script {
                    sh """
                        docker stop ${APP_NAME} || true
                        docker rm ${APP_NAME} || true
                        docker run -d \
                            --name ${APP_NAME} \
                            -p 80:80 \
                            --restart=always \
                            ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}
                    """
                }
            }
        }
    }
}

Dockerfile

FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

实操心得:前端构建用 npm ci 而非 npm install ,因为 ci 会严格比对 package-lock.json ,确保依赖树和本地开发完全一致,避免“在我机器上能跑”的问题。另外, Dockerfile COPY dist/ 必须在 COPY nginx.conf 之后,否则 Nginx 配置不生效。

5. 高频问题排查与避坑指南

5.1 构建失败常见错误速查表

错误现象 根本原因 解决方案 我的实测耗时
Permission denied: /var/jenkins_home/workspace/... Jenkins 容器内用户(jenkins)对挂载卷 /data/jenkins 无写权限 sudo chown -R 1000:1000 /data/jenkins (1000 是 jenkins 用户 UID) 30 秒
Cannot connect to the Docker daemon at unix:///var/run/docker.sock Docker Socket 挂载路径错误或权限不足 检查 docker-compose.yml /var/run/docker.sock:/var/run/docker.sock:ro ,确认宿主机 socket 存在且可读 2 分钟
Failed to push image: unauthorized: authentication required docker.withRegistry() 未传凭据 ID,或凭据 ID 拼写错误 进入 Credentials → System ,确认凭据 ID 和 Pipeline 中 docker.withRegistry(..., 'id') 一致 1 分钟
sh: mvn: not found Maven 镜像未正确拉取,或 agent { docker { image '...' } } 拼写错误 sudo docker exec -it jenkins-master sh ,手动运行 docker run -it maven:3.9-amazoncorretto-17 mvn -v 测试镜像 5 分钟
ERROR: No such container: my-app (Deploy 阶段) docker stop 命令在容器不存在时返回非 0 状态,导致 Pipeline 中断 sh 命令前加 `

5.2 镜像体积过大问题:如何把 1.2GB 的 SpringBoot 镜像压到 280MB

SpringBoot 项目打成 Fat Jar 后,Docker 镜像常超 1GB,导致推送慢、启动慢。优化三板斧:

第一斧:用分层构建(Multi-stage Build)

# 第一阶段:构建
FROM maven:3.9-amazoncorretto-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests

# 第二阶段:运行(只复制 jar,不带 Maven)
FROM amazoncorretto:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]

第二斧:启用 BuildKit 加速
docker-compose.yml 的 Jenkins 服务中加环境变量:

environment:
  - DOCKER_BUILDKIT=1

并在 Jenkinsfile Build Docker Image 阶段, docker.build() 前加:

sh 'export DOCKER_BUILDKIT=1'

第三斧:清理构建缓存
Build 阶段末尾加:

sh 'mvn clean'  // 清理 target/ 目录,避免下次构建时 COPY 过大

效果:某电商后台项目,优化前镜像 1.18GB,优化后 276MB,构建时间从 6 分钟降到 2 分钟 15 秒,推送时间从 4 分钟降到 45 秒。

5.3 权限问题终极解决方案:Jenkins 用户组映射

最顽固的权限问题,往往源于 Jenkins 容器内用户(UID 1000)和宿主机 Docker 组(GID 999)不匹配。解决方法:

  1. 查宿主机 docker 组 GID:

    getent group docker
    # 输出:docker:x:999:jenkins  # 999 就是 GID
    
  2. 修改 docker-compose.yml ,显式指定 GID:

    services:
      jenkins-master:
        # ... 其他配置
        group_add:
          - "999"  # 不再写 "docker",直接写数字 GID
    
  3. 重启: sudo docker-compose down && sudo docker-compose up -d

这招治好了我遇到的所有 Got permission denied while trying to connect to the Docker daemon 报错。原理是:Docker 守护进程只认 GID,不认组名。容器内 jenkins 用户 UID 1000 加入 GID 999 组,就能和宿主机 docker 组无缝通信。

5.4 Pipeline 脚本调试技巧:如何快速定位哪一行报错

Pipeline 报错时,日志常显示 WorkflowScript: 42: Expected a step @ line 42 ,但实际错误可能在 sh 命令内部。我的调试三步法:

  1. 缩小范围 :注释掉 stages 下除 Checkout 外的所有 stage,确认能拉代码;
  2. 逐行验证 :在 Build 阶段的 sh 命令里,先只写 sh 'echo "build start"' ,再加 sh 'mvn -v' ,再加 sh 'mvn clean package' ,每加一行就构建一次;
  3. 进入容器调试 :当某行 sh 报错,立即在 Jenkins 控制台执行:
    sudo docker exec -it jenkins-master sh
    # 然后手动运行报错的命令,观察详细输出
    

个人体会:90% 的 Pipeline 问题,都是 sh 命令里的路径错误(如 target/ 写成 dist/ )或权限错误(如 npm install 用了 sudo )。把命令拿到容器里手动跑一遍,真相立刻浮现。

6. 进阶实践:让 Jenkins 真正融入研发流程

6.1 Git 分支策略驱动部署:dev/test/prod 环境自动分流

很多团队卡在“测试环境怎么自动部署”。答案是: 用 Git 分支名触发不同 Pipeline

在 Jenkins 项目配置中, Source Code Management → Branches to build

  • origin/dev → 对应测试环境;
  • origin/test → 对应预发环境;
  • origin/main → 对应生产环境。

然后在 Jenkinsfile 里用 env.BRANCH_NAME 判断:

environment {
    DOCKER_REGISTRY = 'http://<服务器IP>:5000'
    APP_NAME = 'my-app'
    // 根据分支名动态设置镜像 tag 和部署端口
    IMAGE_TAG = "${env.BRANCH_NAME}-${BUILD_NUMBER}"
    DEPLOY_PORT = env.BRANCH_NAME == 'main' ? '8080' : env.BRANCH_NAME == 'test' ? '8081' : '8082'
}

stage('Deploy') {
    steps {
        script {
            sh """
                docker stop ${APP_NAME}-${env.BRANCH_NAME} || true
                docker rm ${APP_NAME}-${env.BRANCH_NAME} || true
                docker run -d \
                    --name ${APP_NAME}-${env.BRANCH_NAME} \
                    -p ${DEPLOY_PORT}:8080 \
                    --restart=always \
                    ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}
            """
        }
    }
}

效果:推 dev 分支,自动部署到 http://<服务器IP>:8082 ;推 test 分支,