Series Article

Jenkins 自动化部署实战 · GitLab + Maven + Docker + SSH

Jenkins 自动化部署实战

这篇文档从零搭建一套完整的 Jenkins 自动化部署流程,适用于当前 SaaS 柔性供应链项目,也适用于大多数 Spring Boot / Spring Cloud 微服务项目。

目标是实现:

开发者 push 代码到 GitLab

GitLab Webhook 通知 Jenkins

Jenkins 自动拉取代码

Maven 执行测试和打包

Docker 构建镜像

推送镜像到镜像仓库

Jenkins 通过 SSH 登录服务器

执行部署脚本 SSH的脚本

服务器拉取新镜像并重启容器

健康检查

成功通知或失败回滚

这套流程的价值不是“省几条命令”,而是把部署过程标准化。没有流水线时,部署经常依赖人工操作:手动拉代码、手动打包、手动上传 jar、手动重启服务。只要漏一步或者传错包,就可能造成线上事故。Jenkins 的作用是把这些步骤固化成可重复执行、可审计、可回滚的流水线。

第一节 整体架构

1.1 自动化部署全链路

flowchart LR
    DEV["开发者<br>git push"] --> GL["GitLab<br>代码仓库"]
    GL -->|"Webhook 事件<br>Push / Merge"| JK["Jenkins<br>Pipeline Job"]
    JK -->|"git clone / checkout"| SRC["工作空间<br>拉取源码"]
    SRC -->|"mvn test / package"| PKG["Maven<br>测试与打包"]
    PKG -->|"docker build"| IMG["Docker 镜像<br>flexchain-xxx:commitId"]
    IMG -->|"docker push"| REG["镜像仓库<br>Harbor / Docker Registry"]
    JK -->|"ssh 执行部署脚本"| APP["应用服务器"]
    APP -->|"docker pull"| REG
    APP -->|"docker compose up -d"| RUN["运行新容器"]
    RUN -->|"curl /actuator/health"| HC["健康检查"]

核心组件说明:

组件作用
GitLab保存代码,代码 push 后发送 Webhook 事件
Jenkins接收事件,执行流水线
Maven编译、测试、打包 Java 项目
Docker构建应用镜像,保证运行环境一致
镜像仓库保存构建后的 Docker 镜像,例如 Harbor、Docker Registry
SSHJenkins 远程登录应用服务器执行部署脚本
应用服务器拉取镜像、启动容器、执行健康检查

1.2 本文约定的环境信息

下面的地址都是示例,真实部署时替换成自己的环境。

项目示例
GitLab 地址http://gitlab.flexchain.local
Jenkins 地址http://jenkins.flexchain.local:8080
镜像仓库harbor.flexchain.local
后端镜像名harbor.flexchain.local/flexchain/flexchain-backend
前端镜像名harbor.flexchain.local/flexchain/flexchain-web
部署服务器192.168.10.20
部署目录/opt/flexchain
Jenkins 凭据 ID:GitLabgitlab-ssh-key
Jenkins 凭据 ID:Harborharbor-user-pass
Jenkins 凭据 ID:部署服务器prod-server-ssh-key

1.3 推荐仓库结构

flexchain/
├── Jenkinsfile
├── pom.xml
├── flexchain-auth/
├── flexchain-gateway/
├── flexchain-srm/
├── flexchain-pms/
├── flexchain-wms/
├── flexchain-oms/
├── flexchain-tms/
├── flexchain-fms/
├── flexchain-bi/
├── flexchain-common/
├── deploy/
│   ├── docker-compose.prod.yml
│   ├── deploy-backend.sh
│   ├── rollback-backend.sh
│   └── nginx.conf
└── docker/
    ├── Dockerfile.backend
    └── Dockerfile.frontend

Jenkinsfile 放在仓库根目录,由 Jenkins Pipeline 读取。deploy 目录放部署脚本和生产编排文件。docker 目录放 Dockerfile。


第二节 准备应用服务器

2.1 安装 Docker 和 Docker Compose

应用服务器负责运行容器,需要安装 Docker。

# 1. 安装依赖
sudo yum install -y yum-utils device-mapper-persistent-data lvm2

# 2. 添加 Docker 源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 3. 安装 Docker
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# 4. 启动 Docker
sudo systemctl enable docker
sudo systemctl start docker

# 5. 验证
docker version
docker compose version

Ubuntu 系统命令略有不同:

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /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 $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable docker
sudo systemctl start docker

2.2 创建部署目录

sudo mkdir -p /opt/flexchain
sudo mkdir -p /opt/flexchain/logs
sudo mkdir -p /opt/flexchain/backups
sudo mkdir -p /opt/flexchain/scripts
sudo chown -R deploy:deploy /opt/flexchain

这里建议创建一个专门的部署用户 deploy,不要直接用 root 执行部署。

sudo useradd deploy
sudo usermod -aG docker deploy

用户加入 docker 组后,需要重新登录才生效。

2.3 准备生产环境配置

在应用服务器 /opt/flexchain/.env.prod 放生产配置。

APP_ENV=prod
SPRING_PROFILES_ACTIVE=prod

MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_DATABASE=flexchain
MYSQL_USERNAME=flexchain_app
MYSQL_PASSWORD=change_me

REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=change_me

NACOS_HOST=nacos
NACOS_PORT=8848

ROCKETMQ_NAMESRV=rocketmq-namesrv:9876

IMAGE_BACKEND=harbor.flexchain.local/flexchain/flexchain-backend
IMAGE_TAG=latest

.env.prod 不能提交到 GitLab。生产密码、Redis 密码、OSS 密钥、短信密钥等都应该由服务器配置或配置中心管理。

2.4 准备 docker-compose.prod.yml

生产环境的 docker-compose.prod.yml 放在 /opt/flexchain,也可以由 Jenkins 首次部署时上传。

version: "3.9"

services:
  flexchain-backend:
    image: ${IMAGE_BACKEND}:${IMAGE_TAG}
    container_name: flexchain-backend
    restart: always
    env_file:
      - .env.prod
    ports:
      - "8080:8080"
    volumes:
      - ./logs/backend:/app/logs
    networks:
      - flexchain-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 60s

networks:
  flexchain-net:
    external: true

生产环境中 MySQL、Redis、Nacos、RocketMQ 可以和业务服务一起用 compose 编排,也可以由独立中间件集群提供。真实生产更推荐中间件独立部署,应用服务只连接这些中间件。


第三节 准备 Jenkins

3.1 Jenkins 安装方式

Jenkins 可以直接安装到服务器,也可以用 Docker 运行。这里使用 Docker Compose 部署 Jenkins。

在 Jenkins 服务器创建目录:

mkdir -p /opt/jenkins/jenkins_home
mkdir -p /opt/jenkins/maven_repo

创建 /opt/jenkins/docker-compose.yml

version: "3.9"

services:
  jenkins:
    image: jenkins/jenkins:lts-jdk17
    container_name: jenkins
    restart: always
    user: root
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - ./jenkins_home:/var/jenkins_home
      - ./maven_repo:/root/.m2
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker

启动 Jenkins:

cd /opt/jenkins
docker compose up -d
docker logs -f jenkins

首次启动时日志里会打印初始化密码:

Please use the following password to proceed to installation:
xxxxxxxxxxxxxxxxxxxxxxxx

浏览器访问:

http://jenkins.flexchain.local:8080

输入初始密码,安装推荐插件。

3.2 Jenkins 必装插件

进入 Jenkins 后安装以下插件:

插件用途
Git拉取 Git 仓库代码
GitLab接收 GitLab Webhook,展示提交状态
Pipeline支持 Jenkinsfile 流水线
Credentials Binding在流水线中安全读取凭据
SSH Agent使用 SSH 私钥登录远程服务器
Docker Pipeline在 Pipeline 中执行 Docker 构建
Maven Integration支持 Maven 构建
Blue Ocean可视化查看流水线阶段

安装路径:

Manage Jenkins
  -> Plugins
  -> Available plugins
  -> 搜索插件名称
  -> Install

安装完成后重启 Jenkins。

3.3 配置 Maven 和 JDK

如果 Jenkins 容器里已经有 JDK,可以直接使用容器自带 JDK。Maven 可以在 Jenkins 全局工具里配置。

路径:

Manage Jenkins
  -> Tools
  -> JDK installations
  -> Maven installations

配置 Maven:

Name: Maven-3.9
Install automatically: 勾选
Version: 3.9.x

如果公司内网有 Maven 私服,需要准备 settings.xml,并在 Jenkins 中配置 Maven 使用私服。

示例 settings.xml

<settings>
    <mirrors>
        <mirror>
            <id>aliyunmaven</id>
            <mirrorOf>*</mirrorOf>
            <name>阿里云公共仓库</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </mirror>
    </mirrors>
</settings>

3.4 配置 Jenkins 凭据

Jenkins 不应该把密码、私钥写到 Jenkinsfile 中,而是统一放在 Credentials。

路径:

Manage Jenkins
  -> Credentials
  -> System
  -> Global credentials
  -> Add Credentials

需要创建三类凭据。

GitLab SSH 私钥

Kind: SSH Username with private key
ID: gitlab-ssh-key
Username: git
Private Key: GitLab 可访问仓库的私钥

这个凭据用于 Jenkins 拉取 GitLab 代码。

镜像仓库账号密码

Kind: Username with password
ID: harbor-user-pass
Username: harbor 用户名
Password: harbor 密码

这个凭据用于 docker login

生产服务器 SSH 私钥

Kind: SSH Username with private key
ID: prod-server-ssh-key
Username: deploy
Private Key: deploy 用户对应的私钥

这个凭据用于 Jenkins 登录应用服务器执行部署脚本。

3.5 Jenkins 访问 GitLab 的 SSH Key

在 Jenkins 服务器生成 SSH Key:

ssh-keygen -t rsa -b 4096 -C "jenkins@gitlab" -f ~/.ssh/jenkins_gitlab

把公钥内容复制到 GitLab:

cat ~/.ssh/jenkins_gitlab.pub

GitLab 配置位置:

GitLab
  -> User Settings
  -> SSH Keys
  -> Add new key

如果使用 Deploy Key,也可以添加到具体项目:

Project
  -> Settings
  -> Repository
  -> Deploy keys

第四节 GitLab Webhook 触发 Jenkins

4.1 Jenkins 创建 Pipeline Job

Jenkins 首页点击:

New Item

输入任务名:

flexchain-backend-prod

选择:

Pipeline

进入任务配置。

4.2 配置 GitLab 仓库

Pipeline 定义选择:

Pipeline script from SCM

SCM 选择:

Git

Repository URL:

git@gitlab.flexchain.local:flexchain/flexchain.git

Credentials:

gitlab-ssh-key

Branches to build:

*/main

Script Path:

Jenkinsfile

这表示 Jenkins 每次构建时,会先从 GitLab 拉取 main 分支,然后读取仓库根目录的 Jenkinsfile 执行流水线。

4.3 配置构建触发器

在 Jenkins Job 配置中找到:

Build Triggers

勾选:

Build when a change is pushed to GitLab

Jenkins 会显示一个 Webhook 地址,通常类似:

http://jenkins.flexchain.local:8080/project/flexchain-backend-prod

同时建议配置一个 Secret Token,例如:

flexchain-prod-webhook-secret

Secret Token 用于防止别人伪造 GitLab Webhook 请求。

4.4 GitLab 配置 Webhook

进入 GitLab 项目:

Project
  -> Settings
  -> Webhooks

填写 URL:

http://jenkins.flexchain.local:8080/project/flexchain-backend-prod

填写 Secret Token:

flexchain-prod-webhook-secret

勾选触发事件:

Push events
Merge request events
Tag push events

如果只希望 main 分支部署生产,可以在 Jenkinsfile 中判断分支,也可以在 GitLab Webhook 中限制分支。

点击:

Test -> Push events

如果 Jenkins 收到请求,会触发一次构建。

4.5 Webhook 触发后的完整过程

sequenceDiagram
    participant Dev as 开发者
    participant GitLab as GitLab
    participant Jenkins as Jenkins
    participant Repo as Jenkins工作空间

    Dev->>GitLab: git push origin main
    GitLab->>Jenkins: Webhook 推送事件
    Jenkins->>Jenkins: 校验 Secret Token
    Jenkins->>GitLab: 根据 Jenkinsfile SCM 配置拉取代码
    GitLab-->>Repo: 返回最新代码
    Jenkins->>Repo: 执行 Jenkinsfile

这一步的核心是:Jenkins 不是一直轮询代码仓库,而是 GitLab 在代码变化后主动通知 Jenkins。


第五节 Dockerfile 设计

5.1 后端 Dockerfile

docker/Dockerfile.backend

FROM eclipse-temurin:21-jre

WORKDIR /app

ARG JAR_FILE

COPY ${JAR_FILE} /app/app.jar

RUN mkdir -p /app/logs

ENV JAVA_OPTS="-Xms512m -Xmx512m"
ENV SPRING_PROFILES_ACTIVE=prod

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Dspring.profiles.active=$SPRING_PROFILES_ACTIVE -jar /app/app.jar"]

这个 Dockerfile 不负责 Maven 打包,只负责运行 jar。打包由 Jenkins 的 Maven 阶段完成,这样构建过程更清晰。

5.2 为什么镜像里不写生产密码

镜像应该只包含应用程序和运行环境,不应该包含数据库密码、Redis 密码、OSS 密钥等敏感配置。

正确方式是:

镜像:保存代码和运行环境
.env.prod:保存生产配置
Nacos:保存业务配置
Jenkins Credentials:保存流水线敏感凭据

同一个镜像可以部署到测试环境、预发环境、生产环境,只要加载不同配置即可。


第六节 Jenkinsfile 完整示例

6.1 单体后端服务 Pipeline

下面的 Jenkinsfile 适合先讲清楚完整链路。微服务项目可以在这个基础上扩展成多模块构建。

pipeline {
    agent any

    tools {
        maven 'Maven-3.9'
    }

    environment {
        APP_NAME = 'flexchain-backend'
        REGISTRY = 'harbor.flexchain.local'
        IMAGE_REPO = 'harbor.flexchain.local/flexchain/flexchain-backend'
        DEPLOY_HOST = '192.168.10.20'
        DEPLOY_DIR = '/opt/flexchain'
        DOCKERFILE = 'docker/Dockerfile.backend'
    }

    options {
        timestamps()
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '20'))
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        script: "git rev-parse --short HEAD",
                        returnStdout: true
                    ).trim()
                    env.IMAGE_TAG = "${env.BRANCH_NAME ?: 'main'}-${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
                }
                echo "当前构建镜像标签: ${IMAGE_TAG}"
            }
        }

        stage('Maven Test') {
            steps {
                sh 'mvn clean test -B --no-transfer-progress'
            }
            post {
                always {
                    junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml'
                }
            }
        }

        stage('Maven Package') {
            steps {
                sh 'mvn package -DskipTests -B --no-transfer-progress'
            }
        }

        stage('Docker Build') {
            steps {
                sh '''
                    JAR_FILE=$(find . -path "*/target/*.jar" ! -name "*sources.jar" ! -name "*javadoc.jar" | head -n 1)
                    echo "使用 JAR 文件: ${JAR_FILE}"
                    docker build \
                      -f ${DOCKERFILE} \
                      --build-arg JAR_FILE=${JAR_FILE} \
                      -t ${IMAGE_REPO}:${IMAGE_TAG} \
                      -t ${IMAGE_REPO}:latest \
                      .
                '''
            }
        }

        stage('Docker Push') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'harbor-user-pass',
                    usernameVariable: 'HARBOR_USER',
                    passwordVariable: 'HARBOR_PASS'
                )]) {
                    sh '''
                        echo "${HARBOR_PASS}" | docker login ${REGISTRY} -u "${HARBOR_USER}" --password-stdin
                        docker push ${IMAGE_REPO}:${IMAGE_TAG}
                        docker push ${IMAGE_REPO}:latest
                    '''
                }
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sshagent(credentials: ['prod-server-ssh-key']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no deploy@${DEPLOY_HOST} "
                          cd ${DEPLOY_DIR} &&
                          IMAGE_TAG=${IMAGE_TAG} IMAGE_BACKEND=${IMAGE_REPO} bash scripts/deploy-backend.sh
                        "
                    '''
                }
            }
        }

        stage('Health Check') {
            when {
                branch 'main'
            }
            steps {
                sshagent(credentials: ['prod-server-ssh-key']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no deploy@${DEPLOY_HOST} "
                          curl -f http://localhost:8080/actuator/health
                        "
                    '''
                }
            }
        }
    }

    post {
        success {
            echo "流水线执行成功,镜像: ${IMAGE_REPO}:${IMAGE_TAG}"
        }
        failure {
            echo "流水线执行失败,请查看 Jenkins 控制台日志"
        }
        always {
            sh 'docker image prune -f || true'
        }
    }
}

6.2 每个阶段在做什么

阶段作用
Checkout从 GitLab 拉取代码,生成镜像标签
Maven Test执行单元测试,失败则停止流水线
Maven Package打包 jar
Docker Build根据 Dockerfile 构建镜像
Docker Push登录 Harbor,推送镜像
DeploySSH 到服务器执行部署脚本
Health Check调用健康检查接口确认服务可用

6.3 为什么镜像标签要带 commitId

不要只使用 latestlatest 无法追溯具体代码版本,也不方便回滚。

推荐标签格式:

main-8f3a21c-156

含义:

分支名-commit短ID-Jenkins构建号

这样线上容器运行哪个版本,一眼就能追溯到 GitLab 提交记录和 Jenkins 构建记录。


第七节 部署脚本

7.1 deploy-backend.sh

应用服务器 /opt/flexchain/scripts/deploy-backend.sh

#!/bin/bash

set -e

APP_NAME="flexchain-backend"
DEPLOY_DIR="/opt/flexchain"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.prod.yml"
ENV_FILE="${DEPLOY_DIR}/.env.prod"
BACKUP_DIR="${DEPLOY_DIR}/backups"

if [ -z "${IMAGE_TAG}" ]; then
  echo "IMAGE_TAG 不能为空"
  exit 1
fi

if [ -z "${IMAGE_BACKEND}" ]; then
  echo "IMAGE_BACKEND 不能为空"
  exit 1
fi

cd "${DEPLOY_DIR}"

echo "开始部署 ${APP_NAME}"
echo "镜像: ${IMAGE_BACKEND}:${IMAGE_TAG}"

mkdir -p "${BACKUP_DIR}"

if docker ps --format '{{.Names}}' | grep -q "^${APP_NAME}$"; then
  OLD_IMAGE=$(docker inspect --format='{{.Config.Image}}' "${APP_NAME}")
  echo "${OLD_IMAGE}" > "${BACKUP_DIR}/${APP_NAME}.last-image"
  echo "当前旧镜像: ${OLD_IMAGE}"
else
  echo "当前没有运行中的旧容器"
fi

echo "更新 .env.prod 中的镜像配置"
grep -v '^IMAGE_TAG=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_TAG=${IMAGE_TAG}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"

grep -v '^IMAGE_BACKEND=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_BACKEND=${IMAGE_BACKEND}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"

echo "拉取新镜像"
docker pull "${IMAGE_BACKEND}:${IMAGE_TAG}"

echo "启动新版本容器"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d --no-deps "${APP_NAME}"

echo "等待服务启动"
sleep 20

echo "执行健康检查"
for i in {1..10}; do
  if curl -f http://localhost:8080/actuator/health; then
    echo "健康检查通过"
    docker image prune -f
    exit 0
  fi

  echo "第 ${i} 次健康检查失败,等待重试"
  sleep 6
done

echo "健康检查失败,开始回滚"
bash "${DEPLOY_DIR}/scripts/rollback-backend.sh"
exit 1

给脚本执行权限:

chmod +x /opt/flexchain/scripts/deploy-backend.sh

7.2 rollback-backend.sh

应用服务器 /opt/flexchain/scripts/rollback-backend.sh

#!/bin/bash

set -e

APP_NAME="flexchain-backend"
DEPLOY_DIR="/opt/flexchain"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.prod.yml"
ENV_FILE="${DEPLOY_DIR}/.env.prod"
BACKUP_FILE="${DEPLOY_DIR}/backups/${APP_NAME}.last-image"

if [ ! -f "${BACKUP_FILE}" ]; then
  echo "没有找到上一版本镜像记录,无法自动回滚"
  exit 1
fi

OLD_IMAGE=$(cat "${BACKUP_FILE}")

if [ -z "${OLD_IMAGE}" ]; then
  echo "上一版本镜像为空,无法回滚"
  exit 1
fi

OLD_REPO=$(echo "${OLD_IMAGE}" | cut -d ':' -f 1)
OLD_TAG=$(echo "${OLD_IMAGE}" | cut -d ':' -f 2)

echo "开始回滚到 ${OLD_IMAGE}"

grep -v '^IMAGE_BACKEND=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_BACKEND=${OLD_REPO}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"

grep -v '^IMAGE_TAG=' "${ENV_FILE}" > "${ENV_FILE}.tmp"
echo "IMAGE_TAG=${OLD_TAG}" >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"

docker pull "${OLD_IMAGE}"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d --no-deps "${APP_NAME}"

sleep 20

curl -f http://localhost:8080/actuator/health

echo "回滚完成"

给脚本执行权限:

chmod +x /opt/flexchain/scripts/rollback-backend.sh

7.3 为什么部署脚本要放服务器

Jenkinsfile 负责流水线控制,服务器脚本负责具体部署动作。这样有几个好处:

  • Jenkinsfile 不会塞满复杂 shell。
  • 服务器部署逻辑可以单独测试。
  • 手动应急时也可以直接执行脚本。
  • 回滚逻辑和部署逻辑放在同一台服务器,更容易读取当前容器状态。

第八节 微服务项目如何扩展

8.1 单服务构建

如果只构建某一个微服务,例如 OMS:

mvn clean package -pl flexchain-oms -am -DskipTests

含义:

参数说明
-pl flexchain-oms只构建 OMS 模块
-am同时构建 OMS 依赖的模块
-DskipTests打包阶段跳过测试,测试已在前面阶段执行

8.2 Jenkins 参数化构建

可以把 Jenkins Job 做成参数化,选择部署哪个服务。

Jenkinsfile 示例:

parameters {
    choice(
        name: 'SERVICE_NAME',
        choices: ['flexchain-auth', 'flexchain-gateway', 'flexchain-oms', 'flexchain-wms', 'flexchain-tms'],
        description: '选择要构建和部署的服务'
    )
}

打包命令:

sh "mvn clean package -pl ${params.SERVICE_NAME} -am -DskipTests -B --no-transfer-progress"

镜像名:

environment {
    IMAGE_REPO = "harbor.flexchain.local/flexchain/${params.SERVICE_NAME}"
}

8.3 多服务批量部署

如果一次提交影响多个服务,可以设计服务列表:

def services = ['flexchain-auth', 'flexchain-gateway', 'flexchain-oms']

for (svc in services) {
    sh "mvn clean package -pl ${svc} -am -DskipTests"
    sh "docker build -f docker/Dockerfile.backend -t ${REGISTRY}/flexchain/${svc}:${IMAGE_TAG} ."
    sh "docker push ${REGISTRY}/flexchain/${svc}:${IMAGE_TAG}"
}

批量部署要谨慎。生产环境更推荐一次只发布一个或少量服务,避免多个服务同时异常时难以定位。


第九节 部署过程中的关键细节

9.1 为什么测试失败不能继续部署

流水线最重要的原则是:

测试失败,禁止部署。

如果单元测试或集成测试失败还继续构建镜像,就会把已知有问题的代码推到测试环境甚至生产环境。Jenkinsfile 中 mvn test 失败时,后续阶段会自动停止。

9.2 为什么 Jenkins 要使用凭据

GitLab 私钥、Harbor 密码、服务器 SSH 私钥都属于敏感信息,不能写在 Jenkinsfile、shell 脚本或 Git 仓库里。

正确做法是:

Jenkins Credentials 保存敏感信息。
Jenkinsfile 通过 credentialsId 引用。
构建日志中不打印密码。

9.3 为什么要健康检查

容器启动成功不代表服务可用。Spring Boot 启动可能失败,数据库连接可能失败,Redis 可能不可用,Nacos 配置可能拉取失败。

健康检查接口:

GET /actuator/health

只有健康检查通过,才能认为部署成功。

9.4 为什么要保留上一版本镜像

部署失败时需要回滚。回滚不是重新打包,而是让服务器重新启动上一版本镜像。

所以部署前要记录旧镜像:

docker inspect --format='{{.Config.Image}}' flexchain-backend

保存到:

/opt/flexchain/backups/flexchain-backend.last-image

9.5 为什么不要把 .env.prod 提交到 GitLab

.env.prod 里有数据库密码、Redis 密码、OSS 密钥、短信密钥等敏感配置。提交到 GitLab 会造成安全风险。正确做法是:

  • 本地提交 .env.example 作为模板。
  • 生产服务器保存真实 .env.prod
  • Jenkins 只传镜像版本,不传生产密码。

9.6 为什么 Jenkins 服务器需要访问 Docker

Jenkins 要执行:

docker build
docker push

如果 Jenkins 运行在 Docker 容器里,需要挂载宿主机 Docker Socket:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock
  - /usr/bin/docker:/usr/bin/docker

这样 Jenkins 容器里的 docker 命令实际操作的是宿主机 Docker。

这种方式方便,但也要注意安全:拥有 Docker Socket 权限基本等价于拥有宿主机较高权限,所以 Jenkins 服务器要限制访问。


第十节 前端项目如何部署

前端一般是 Vue 或 React 项目。部署方式是:Jenkins 拉代码,执行 npm installnpm run build,然后构建 Nginx 镜像或把 dist 上传到服务器。

10.1 前端 Dockerfile

docker/Dockerfile.frontend

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm config set registry https://registry.npmmirror.com
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:1.25-alpine

COPY --from=builder /app/dist /usr/share/nginx/html
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

10.2 前端 Jenkins 阶段

stage('Build Frontend Image') {
    steps {
        sh '''
            docker build \
              -f docker/Dockerfile.frontend \
              -t harbor.flexchain.local/flexchain/flexchain-web:${IMAGE_TAG} \
              -t harbor.flexchain.local/flexchain/flexchain-web:latest \
              .
        '''
    }
}

stage('Push Frontend Image') {
    steps {
        withCredentials([usernamePassword(
            credentialsId: 'harbor-user-pass',
            usernameVariable: 'HARBOR_USER',
            passwordVariable: 'HARBOR_PASS'
        )]) {
            sh '''
                echo "${HARBOR_PASS}" | docker login harbor.flexchain.local -u "${HARBOR_USER}" --password-stdin
                docker push harbor.flexchain.local/flexchain/flexchain-web:${IMAGE_TAG}
                docker push harbor.flexchain.local/flexchain/flexchain-web:latest
            '''
        }
    }
}

前端和后端可以放在同一个 Jenkinsfile 里,也可以拆成两个 Job。中小项目可以合并,微服务较多时建议拆分。


第十一节 常见问题排查

11.1 GitLab Webhook 没有触发 Jenkins

检查点:

  1. GitLab Webhook URL 是否正确。
  2. Jenkins Job 是否勾选 GitLab 触发器。
  3. Secret Token 是否一致。
  4. Jenkins 地址是否能被 GitLab 访问。
  5. Jenkins GitLab 插件是否安装。
  6. GitLab Webhook 测试返回是否 200。

GitLab Webhook 页面会显示最近请求记录,可以查看返回状态码和错误信息。

11.2 Jenkins 拉代码失败

常见原因:

  • GitLab SSH Key 没配置。
  • Jenkins Credentials 选错。
  • 仓库地址写错。
  • Jenkins 服务器无法访问 GitLab。
  • GitLab Deploy Key 没有项目权限。

可以在 Jenkins 服务器测试:

ssh -T git@gitlab.flexchain.local

11.3 Maven 下载依赖很慢

解决方式:

  • 配置 Maven 镜像源。
  • 使用公司 Nexus 私服。
  • 挂载 Maven 本地仓库目录。
  • Jenkins Pipeline 开启 Maven 缓存。

11.4 Docker build 失败

常见原因:

  • Dockerfile 路径错误。
  • jar 文件路径没找到。
  • Jenkins 容器没有 docker 命令。
  • Jenkins 没有访问 Docker Socket 权限。
  • 基础镜像拉取失败。

排查命令:

docker version
docker images
find . -path "*/target/*.jar"

11.5 Docker push 失败

常见原因:

  • Harbor 地址错误。
  • 账号密码错误。
  • 镜像命名不符合仓库规则。
  • Harbor 项目不存在。
  • Jenkins 服务器无法访问 Harbor。

手动测试:

docker login harbor.flexchain.local
docker push harbor.flexchain.local/flexchain/flexchain-backend:test

11.6 SSH 部署失败

常见原因:

  • Jenkins 凭据 ID 不正确。
  • deploy 用户私钥错误。
  • 服务器防火墙不允许 22 端口。
  • deploy 用户没有 docker 权限。
  • /opt/flexchain 权限不足。

手动测试:

ssh deploy@192.168.10.20
docker ps
cd /opt/flexchain

11.7 容器启动成功但健康检查失败

常见原因:

  • Spring Boot 启动失败。
  • 端口映射错误。
  • .env.prod 配置错误。
  • 数据库、Redis、Nacos 不可用。
  • Actuator 没开放 health 端点。

排查命令:

docker logs -f flexchain-backend
docker inspect flexchain-backend
curl -v http://localhost:8080/actuator/health

第十二节 面试怎么讲

12.1 简历写法

可以这样写:

负责项目自动化部署流水线建设,基于 Jenkins + GitLab Webhook + Maven + Docker + Harbor + SSH 实现从代码提交到测试、打包、镜像构建、镜像推送、服务器部署、健康检查和失败回滚的完整 CI/CD 流程。通过 Jenkins Credentials 管理 GitLab、镜像仓库和生产服务器凭据,避免敏感信息写入代码仓库。

12.2 面试表达

可以这样讲:

“我们项目使用 Jenkins 做自动化部署。开发者把代码 push 到 GitLab 后,GitLab 通过 Webhook 通知 Jenkins。Jenkins 接收到事件后,根据 Pipeline Job 配置从 GitLab 拉取代码,然后执行 Jenkinsfile。

流水线分成几个阶段。第一步是 Checkout,拉取代码并生成镜像标签,标签里包含分支、commitId 和 Jenkins 构建号,方便追溯版本。第二步是 Maven Test,执行单元测试,测试失败就直接终止流水线。第三步是 Maven Package,把 Spring Boot 项目打成 jar。第四步是 Docker Build,根据 Dockerfile 构建镜像。第五步是 Docker Push,把镜像推送到 Harbor。第六步是 Deploy,Jenkins 使用 SSH 凭据登录生产服务器,执行部署脚本。服务器脚本会拉取新镜像、更新 docker-compose、启动新容器,并调用 /actuator/health 做健康检查。如果健康检查失败,就自动回滚到上一版本镜像。

这里有几个关键点。第一,敏感信息不写在 Jenkinsfile 里,而是放在 Jenkins Credentials。第二,镜像标签不用单纯 latest,而是带 commitId 和 build number,方便追溯和回滚。第三,部署脚本放在服务器上,Jenkins 只负责触发,这样应急时也能手动执行。第四,容器启动不等于服务可用,必须做健康检查。”

12.3 高频问题

Q1:GitLab push 后 Jenkins 是怎么知道的?

答:通过 GitLab Webhook。GitLab 项目配置 Jenkins Job 的 Webhook URL。代码 push 或合并请求发生时,GitLab 主动向 Jenkins 发送 HTTP 请求,Jenkins 收到后触发 Pipeline。

Q2:Jenkins 如何拉取 GitLab 代码?

答:Jenkins Job 配置 SCM 为 Git,填写 GitLab SSH 仓库地址,并选择 GitLab SSH 私钥凭据。构建时 Jenkins 使用这个凭据执行 git clone 或 checkout。

Q3:为什么测试失败不能继续部署?

答:测试失败说明当前代码已经存在确定问题。如果继续打包部署,会把有问题的代码推到环境中。流水线应该把测试作为质量门禁,测试失败后直接停止。

Q4:为什么要构建 Docker 镜像?

答:Docker 镜像把应用、JRE、启动命令和运行环境打包在一起,避免不同服务器环境不一致。镜像可以在测试、预发、生产复用,只需要加载不同配置。

Q5:为什么镜像标签不能只用 latest?

答:latest 无法追溯具体代码版本,也不方便回滚。推荐使用 分支-commitId-构建号 作为镜像标签,线上运行哪个版本可以直接对应到 GitLab 提交和 Jenkins 构建记录。

Q6:Jenkins 如何登录生产服务器?

答:通过 SSH Agent 插件和 Jenkins Credentials 中保存的 SSH 私钥。Jenkinsfile 中使用 sshagent(credentials: ['prod-server-ssh-key']),然后执行 ssh 命令登录服务器运行部署脚本。

Q7:为什么部署脚本不全部写在 Jenkinsfile 里?

答:Jenkinsfile 负责流水线编排,服务器部署脚本负责具体部署动作。分开后结构更清晰,也方便在服务器上手动应急执行部署或回滚。

Q8:如何实现失败回滚?

答:部署前记录当前运行容器的旧镜像。新镜像启动后执行健康检查,如果连续失败,就读取旧镜像记录,修改 .env.prod 中的镜像标签,重新启动旧版本容器。

Q9:如何保证生产配置安全?

答:生产密码不写在 GitLab 和 Jenkinsfile 里。流水线凭据放 Jenkins Credentials,应用配置放服务器 .env.prod 或 Nacos 配置中心。Git 仓库只提交 .env.example

Q10:Jenkins 和 GitLab、Harbor、服务器之间需要哪些凭据?

答:Jenkins 拉 GitLab 需要 GitLab SSH 私钥;推送镜像到 Harbor 需要 Harbor 账号密码;SSH 登录服务器需要部署用户私钥。这些都放在 Jenkins Credentials 中,通过 credentialsId 引用。


第十三节 最终流程清单

上线前检查:

  1. GitLab 仓库已创建,Jenkins SSH Key 有读取权限。
  2. Jenkins 已安装 Git、GitLab、Pipeline、Docker、SSH Agent、Credentials Binding 插件。
  3. Jenkins 已配置 GitLab、Harbor、生产服务器三类凭据。
  4. GitLab Webhook 已配置并测试成功。
  5. Jenkinsfile 已提交到仓库根目录。
  6. Dockerfile 能在 Jenkins 服务器构建成功。
  7. Harbor 项目已创建,Jenkins 能登录并 push 镜像。
  8. 应用服务器已安装 Docker 和 Docker Compose。
  9. /opt/flexchain/.env.prod 已准备完成。
  10. /opt/flexchain/docker-compose.prod.yml 已准备完成。
  11. /opt/flexchain/scripts/deploy-backend.sh 有执行权限。
  12. /opt/flexchain/scripts/rollback-backend.sh 有执行权限。
  13. Spring Boot 已开放 /actuator/health
  14. Jenkins 构建成功后能 SSH 到服务器执行部署。
  15. 新容器启动后健康检查通过。

完整部署链路:

git push
-> GitLab Webhook
-> Jenkins Pipeline
-> checkout
-> mvn test
-> mvn package
-> docker build
-> docker push
-> ssh deploy@server
-> docker pull
-> docker compose up -d
-> curl /actuator/health
-> success or rollback