Skip to content

Docker 多阶段构建 + Jenkins CI/CD:Java 微服务容器化部署最佳实践

作者: 必码 | bima.cc


第一章 容器化概述

1.1 Java 应用容器化的挑战

Java 应用容器化看似简单——把 JAR 包塞进镜像、运行 java -jar 即可。但在实际生产环境中,Java 应用的容器化面临着一系列独特的挑战,这些挑战与 Java 语言本身的特性密切相关。

1.1.1 Fat JAR 的体积问题

Spring Boot 的核心部署方式是 Fat JAR(也称为 Uber JAR),它将应用代码、所有第三方依赖库、甚至内嵌的 Tomcat 服务器全部打包到一个可执行 JAR 文件中。这种"一个包搞定一切"的方式极大地简化了部署流程,但也带来了显著的体积膨胀问题。

以 smart-scaffold 系列项目为例,一个典型的 SpringBoot 版 Fat JAR 体积通常在 80MB 到 150MB 之间。这个数字包含了:

  • 应用自身代码:通常仅 1-5MB
  • Spring Boot Starter 依赖族:约 30-50MB
  • 数据库驱动和连接池:约 10-20MB
  • 消息队列客户端(Kafka、RabbitMQ、RocketMQ):约 15-30MB
  • 搜索引擎客户端(Elasticsearch):约 10-15MB
  • 其他工具库(JSON 处理、HTTP 客户端等):约 10-20MB

当我们将这个 Fat JAR 放入 Docker 镜像时,还需要叠加基础镜像的体积。一个标准的 OpenJDK 基础镜像通常在 300MB 到 500MB 之间。这意味着最终的生产镜像可能达到 400MB 到 650MB。

体积膨胀带来的直接影响包括:

  1. 镜像拉取时间延长: 在 CI/CD 流水线中,每次部署都需要拉取镜像。一个 500MB 的镜像在良好的网络环境下需要 30 秒到 1 分钟,而在网络条件较差的环境下可能需要数分钟。
  2. 存储空间消耗: Docker 镜像采用分层存储,每一层都会占用磁盘空间。在频繁部署的场景下,大量历史镜像层会快速消耗服务器存储。
  3. 安全扫描时间增加: 镜像体积越大,安全扫描所需的时间越长,这在自动化流水线中会成为瓶颈。
  4. 容器启动速度降低: 虽然容器启动速度主要取决于应用本身,但较大的镜像层解压也需要额外时间。

1.1.2 JVM 参数调优

Java 虚拟机(JVM)的参数调优是容器化部署中另一个核心挑战。传统虚拟机或物理机部署时,运维人员通常根据硬件配置手动设置 JVM 参数。但在容器化环境中,情况发生了根本性变化。

容器环境下的 JVM 参数挑战:

  1. 内存感知问题: 早期版本的 JVM 无法感知容器的内存限制(cgroups),-Xmx 参数设置不当可能导致容器被 OOM Killer 杀掉。从 Java 8u191 和 Java 11 开始,JVM 才较好地支持了容器内存感知,但仍需注意一些边界情况。

  2. CPU 感知问题: 类似地,JVM 的 GC 线程数默认基于物理 CPU 核心数计算,而非容器分配的 CPU 配额。这可能导致 GC 线程数过多,反而降低性能。

  3. 堆内存与直接内存的平衡: 在容器化环境中,堆内存(Heap)和直接内存(Direct Memory)、元空间(Metaspace)、线程栈(Thread Stack)等都需要纳入整体内存预算。一个常见的错误是将 -Xmx 设置为容器内存限制的 100%,忽略了其他内存区域的需求。

典型的容器化 JVM 参数配置示例:

bash
# 假设容器内存限制为 1GB
java -Xms512m -Xmx768m \
     -XX:MaxMetaspaceSize=128m \
     -XX:MaxDirectMemorySize=64m \
     -XX:+UseG1GC \
     -XX:+UseContainerSupport \
     -XX:MaxGCPauseMillis=200 \
     -jar /docker/app.jar

在上述配置中,我们为堆内存分配了 768MB(容器总内存的 75%),为元空间预留了 128MB,为直接内存预留了 64MB,总共约 960MB,留有约 40MB 的缓冲空间给操作系统和其他开销。

1.1.3 类加载机制

Java 的类加载机制在容器化环境中也面临特殊挑战。Spring Boot Fat JAR 使用了自定义的类加载器 LaunchedURLClassLoader,它与标准的 URLClassLoader 有显著差异。

Fat JAR 的类加载层次:

Bootstrap ClassLoader
    └── Platform ClassLoader
        └── App ClassLoader
            └── LaunchedURLClassLoader (Spring Boot 自定义)
                ├── BOOT-INF/classes/ (应用代码)
                └── BOOT-INF/lib/ (第三方依赖)

容器化环境中的类加载问题:

  1. 类冲突: 当应用依赖的库版本与基础镜像中自带的库版本不一致时,可能产生类冲突。使用多阶段构建可以有效避免这一问题,因为运行阶段的基础镜像只包含 JRE,不包含额外的库。
  2. 热部署限制: 在开发环境中,Spring Boot DevTools 提供了热重载功能。但在容器化部署中,由于文件系统的隔离性,热部署机制需要特殊配置才能工作。
  3. JNI 库加载: 如果应用依赖本地库(JNI),在容器化环境中需要确保这些库在运行阶段的基础镜像中可用。
  4. 类路径隔离: 在微服务架构中,不同服务可能依赖同一库的不同版本。容器化天然提供了进程级隔离,每个服务运行在独立的容器中,从根本上避免了类路径冲突。

1.1.4 容器化与 Java 版本选择

在 smart-scaffold 系列项目中,我们统一选择了 Java 17 作为目标版本。这个选择并非随意,而是基于以下考量:

特性Java 8Java 11Java 17Java 21
LTS 支持是(已过期)
容器感知部分支持较好支持完全支持完全支持
启动速度基准+10%+20%+30%(GraalVM)
内存效率基准+10%+15%+25%
Spring Boot 3.x不支持支持最佳支持最佳支持
Docker 镜像优化有限较好最佳最佳

Java 17 是当前企业级应用的最佳选择:它拥有长期支持(LTS)、对容器化有原生支持、与 Spring Boot 3.x 完美契合,并且在性能和安全性方面都有显著提升。

1.2 多阶段构建的必要性

1.2.1 传统单阶段构建的问题

在没有多阶段构建的时代,Java 应用的 Docker 镜像通常采用以下方式之一:

方式一:在宿主机构建,COPY 进镜像

dockerfile
# 传统方式:在宿主机构建
FROM azul/zulu-openjdk:17
WORKDIR /docker
COPY target/smart-scaffold-web-1.0.0-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/docker/app.jar"]

这种方式的问题在于:它要求 CI/CD 环境和开发环境保持完全一致的构建工具链(Maven 版本、JDK 版本、依赖缓存等),违反了 Docker "Build Once, Run Anywhere" 的核心承诺。

方式二:在镜像内完成全部构建

dockerfile
# 传统方式:全部在镜像内构建(无多阶段)
FROM maven:3.9.9-eclipse-temurin-17
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
# 问题:最终镜像包含 Maven、JDK、源代码、构建中间产物...
CMD ["java", "-jar", "target/smart-scaffold-web-1.0.0-SNAPSHOT.jar"]

这种方式虽然保证了构建环境的一致性,但最终镜像体积巨大。以 Maven 基础镜像为例:

  • maven:3.9.9-eclipse-temurin-17 基础镜像:约 600MB
  • Maven 本身及插件:约 150MB
  • 项目源代码:约 5MB
  • Maven 本地仓库(所有依赖):约 500MB-1GB
  • 最终 Fat JAR:约 100MB

最终镜像体积可能超过 1.5GB,其中真正运行时需要的只有 JRE 和 Fat JAR,大约 400MB。其余 1.1GB 都是构建时产物,在运行时完全无用,却占据了宝贵的存储和传输带宽。

1.2.2 多阶段构建的核心思想

Docker 多阶段构建(Multi-Stage Build)是 Docker 17.05 引入的特性,它允许在一个 Dockerfile 中定义多个构建阶段,每个阶段可以使用不同的基础镜像,并且可以选择性地将前一阶段的产物复制到后续阶段。

核心原理图解:

+---------------------------------------------------+
|         阶段1:构建阶段 (builder)                    |
|  基础镜像: maven:3.9.9-eclipse-temurin-17           |
|  +---------------------------------------------+  |
|  |  Maven + JDK + 源代码 + 依赖仓库              |  |
|  |  -> mvn clean package -DskipTests            |  |
|  |  -> 生成 Fat JAR                              |  |
|  +---------------------------------------------+  |
|  产物: /app/build/app.jar                         |
+------------------------+--------------------------+
                         | COPY --from=builder
                         v
+---------------------------------------------------+
|         阶段2:运行阶段 (runner)                     |
|  基础镜像: azul/zulu-openjdk:17                     |
|  +---------------------------------------------+  |
|  |  仅 JRE + Fat JAR + entrypoint.sh             |  |
|  +---------------------------------------------+  |
|  最终镜像: ~400MB (vs 1.5GB 单阶段)                 |
+---------------------------------------------------+

多阶段构建的关键优势:

  1. 镜像体积缩减 60-75%: 从 1.5GB 降至 400MB,大幅节省存储和传输成本。
  2. 攻击面最小化: 运行时镜像不包含源代码、构建工具和开发依赖,显著降低了安全风险。
  3. 构建一致性: 构建环境完全封装在 Docker 镜像中,消除了"在我机器上能跑"的问题。
  4. 缓存友好: Docker 的层缓存机制可以复用未变化的构建层,加速后续构建。
  5. 灵活性: 不同的阶段可以使用完全不同的基础镜像,构建阶段用 Maven 镜像,运行阶段用精简的 JRE 镜像。

1.2.3 smart-scaffold 项目的镜像体积对比

基于实际测试数据,以下是 smart-scaffold 三个版本在不同构建策略下的镜像体积对比:

构建策略SpringBoot 版Dubbo ProviderSpringCloud Provider
单阶段(全量构建)~1.2GB~1.4GB~1.5GB
多阶段构建~420MB~450MB~480MB
多阶段 + JRE 精简~380MB~400MB~420MB
体积缩减比例68%71%72%

1.3 三个项目的容器化方案对比

smart-scaffold 系列包含三个不同架构的 Java 微服务项目,它们在容器化策略上各有特点。

1.3.1 项目架构概览

SpringBoot 版(单体架构):

smart-scaffold-springboot/
├── smart-scaffold-common/    # 通用工具模块
├── smart-scaffold-dao/       # 数据访问模块
├── smart-scaffold-service/   # 业务逻辑模块
├── smart-scaffold-web/       # Web 层(可执行 JAR)
├── Dockerfile                # 单一 Dockerfile
└── entrypoint.sh             # 启动脚本

SpringBoot 版采用经典的分层单体架构,最终打包为一个可执行的 Fat JAR。容器化策略最为简单:一个 Dockerfile 构建一个镜像,部署一个容器。

Dubbo 版(RPC 微服务架构):

smart-scaffold-dubbo/
├── smart-scaffold-api/           # API 接口定义
├── smart-scaffold-provider/      # 服务提供者(可执行 JAR)
│   ├── Dockerfile
│   └── entrypoint.sh
├── smart-scaffold-consumer/      # 服务消费者(可执行 JAR)
│   ├── Dockerfile
│   └── entrypoint.sh
└── pom.xml

Dubbo 版采用 RPC 微服务架构,Provider 和 Consumer 是两个独立部署的服务,各自拥有自己的 Dockerfile。

SpringCloud 版(REST 微服务架构):

smart-scaffold-springcloud/
├── smart-scaffold-common/        # 通用模块
├── smart-scaffold-provider/      # 服务提供者
│   ├── Dockerfile
│   └── entrypoint.sh
├── smart-scaffold-consumer/      # 服务消费者
│   ├── Dockerfile
│   └── entrypoint.sh
└── pom.xml

SpringCloud 版采用 REST 微服务架构,通过 OpenFeign 进行服务间调用,通过 Zookeeper 进行服务发现。

1.3.2 容器化方案对比矩阵

维度SpringBoot 版Dubbo 版SpringCloud 版
镜像数量1 个2 个2 个
Dockerfile 数量1 个2 个2 个
构建复杂度
网络依赖ZookeeperZookeeper
部署顺序单步Provider -> ConsumerProvider -> Consumer
服务发现N/AZookeeperZookeeper
CI/CD 复杂度
适合场景中小型项目、快速迭代高性能 RPC 场景云原生微服务场景

1.3.3 为什么选择不同的容器化策略

这三个项目虽然属于同一系列,但它们的架构差异决定了不同的容器化策略:

  1. SpringBoot 版采用"全量构建"模式——Dockerfile 内部完成从源代码到可执行 JAR 的全部构建过程。这是因为单体应用只有一个可部署单元,构建流程简单直接。

  2. Dubbo/SpringCloud 版采用"预构建"模式——Dockerfile 假设 JAR 已经构建完成,只负责提取和打包。这是因为微服务架构中,多个模块之间存在依赖关系,通常在 CI/CD 流水线的 Maven 构建阶段统一编译所有模块,然后分别构建各自的 Docker 镜像。

这种策略差异体现了架构设计对 DevOps 实践的深刻影响。在后续章节中,我们将深入分析每种策略的具体实现。


第二章 Docker 多阶段构建详解

2.1 多阶段构建原理

2.1.1 Docker 构建系统基础

在深入多阶段构建之前,我们需要理解 Docker 构建系统的几个核心概念:

1. 镜像层(Image Layer)

Docker 镜像由多个只读层组成,每一层对应 Dockerfile 中的一条指令。当 Docker 构建镜像时,它会检查每条指令是否产生了变化——如果没有变化,就复用缓存的层。这就是为什么 Dockerfile 中指令的顺序对构建效率至关重要。

指令1: FROM maven:3.9.9-eclipse-temurin-17    -> 层1(基础镜像层)
指令2: WORKDIR /app                            -> 层2(元数据层,几乎无体积)
指令3: COPY pom.xml .                          -> 层3(依赖定义)
指令4: RUN mvn dependency:resolve              -> 层4(依赖缓存)
指令5: COPY src/ ./src/                        -> 层5(源代码)
指令6: RUN mvn clean package -DskipTests       -> 层6(构建产物)

在上述示例中,层3和层4利用了 Docker 的缓存机制——只要 pom.xml 没有变化,依赖解析的结果就可以被缓存。这意味着日常开发中修改源代码时,不需要重新下载依赖,大幅加速了构建过程。

2. 构建上下文(Build Context)

当执行 docker build 命令时,Docker 客户端会将构建上下文(通常是指定的目录)中的所有文件发送给 Docker daemon。这就是为什么 .dockerignore 文件如此重要——它排除不需要的文件,减少构建上下文的传输量。

dockerignore
# .dockerignore 示例
.git
.gitignore
.idea
*.iml
target/
node_modules/
*.md
.DS_Store

3. 构建缓存(Build Cache)

Docker 的构建缓存是加速构建的关键。缓存命中规则如下:

  • 对于 ADDCOPY 指令,Docker 会计算文件的校验和。如果文件内容没有变化,则命中缓存。
  • 对于其他指令(RUNENV 等),Docker 会比较指令字符串是否完全一致。

2.1.2 多阶段构建的工作机制

多阶段构建的本质是在一个 Dockerfile 中定义多个独立的构建过程,每个过程都有自己的基础镜像和文件系统。阶段之间通过 COPY --from=<stage> 指令传递产物。

工作机制详解:

时间线 -------------------------------------------------->

阶段1 (builder):
  FROM maven:3.9.9-eclipse-temurin-17 AS builder
  WORKDIR /app
  COPY . .
  RUN mvn clean package -DskipTests -T 1C
  RUN cp target/*.jar /app/build/app.jar
  ---- 阶段1结束,产物保存在 /app/build/app.jar ----
  ---- 阶段1的文件系统被丢弃(除非被后续阶段引用)----

阶段2 (runner):
  FROM azul/zulu-openjdk:17 AS runner
  WORKDIR /docker
  COPY --from=builder /app/build/app.jar /docker/app.jar
  COPY entrypoint.sh /docker/entrypoint.sh
  RUN chmod +x /docker/entrypoint.sh
  ---- 阶段2结束,生成最终镜像 ----

关键要点:

  1. 阶段命名: 使用 AS builderAS runner 为阶段命名,使 COPY --from=builder 更加清晰可读。
  2. 选择性复制: 只有通过 COPY --from 显式复制的文件才会出现在最终镜像中。
  3. 独立缓存: 每个阶段有独立的构建缓存。修改阶段2的指令不会使阶段1的缓存失效。

2.2 构建阶段详解

2.2.1 基础镜像选择

smart-scaffold 项目的构建阶段统一使用 maven:3.9.9-eclipse-temurin-17 作为基础镜像。这个选择经过了深思熟虑:

镜像组成分析:

maven:3.9.9-eclipse-temurin-17
├── Ubuntu 22.04 LTS (基础操作系统)
├── Eclipse Temurin JDK 17 (Java 开发工具包)
│   ├── javac (Java 编译器)
│   ├── java (Java 运行时)
│   ├── jar (JAR 工具)
│   └── 其他 JDK 工具
├── Maven 3.9.9 (项目构建工具)
│   ├── mvn (Maven 命令)
│   ├── 内置插件
│   └── 默认 settings.xml
└── CA 证书、常用工具 (curl、git 等)

为什么选择 Eclipse Temurin 而非其他 JDK 发行版:

JDK 发行版维护方许可证容器优化推荐度
Eclipse TemurinEclipse 基金会GPL v2 + CE良好推荐
Azul ZuluAzul SystemsGPL v2 + CE优秀推荐
Amazon CorrettoAWSGPL v2 + CE良好推荐
Oracle JDKOracleOTN(商业)一般不推荐

Eclipse Temurin 是 OpenJDK 的官方构建版本之一,由 Eclipse 基金会维护,拥有良好的社区支持和安全更新节奏。对于构建阶段,我们选择 Temurin;对于运行阶段,我们选择 Azul Zulu。

2.2.2 Maven 本地仓库配置

在 smart-scaffold-springboot 的 Dockerfile 中,有一段关键的 Maven 配置:

dockerfile
# 优化 Maven 构建配置
RUN mkdir -p ~/.m2 \
    && echo "<settings><localRepository>/tmp/m2repo</localRepository><offline>false</offline><mirrors></mirrors></settings>" > ~/.m2/settings.xml

这段代码做了以下几件事:

  1. 创建 .m2 目录: mkdir -p ~/.m2 确保 Maven 配置目录存在。
  2. 设置本地仓库路径: <localRepository>/tmp/m2repo</localRepository> 将 Maven 本地仓库设置到 /tmp/m2repo。选择 /tmp 目录的原因是它在容器重启后会被自动清理,避免仓库缓存无限增长。
  3. 禁用离线模式: <offline>false</offline> 确保 Maven 在需要时可以从远程仓库下载依赖。
  4. 清空镜像配置: <mirrors></mirrors> 使用空的镜像列表,这意味着将使用 Maven Central 作为默认仓库。

为什么不在构建阶段配置阿里云镜像?

这是一个有意为之的设计决策。在构建阶段,我们使用 Docker 层缓存来加速构建——只要 pom.xml 不变,依赖下载层就可以被缓存。如果在这里配置了阿里云镜像,那么镜像配置的变化会导致缓存失效。阿里云镜像加速更适合在 CI/CD 的 Maven 构建阶段或开发环境中配置。

2.2.3 构建命令解析

SpringBoot 版 Dockerfile 中的构建命令如下:

dockerfile
RUN chmod 755 ./mvnw || true \
    && (if [ -f ./mvnw ]; then ./mvnw clean package -DskipTests -T 1C; else mvn clean package -DskipTests -T 1C; fi)

这段命令包含了多个精心设计的细节:

1. Maven Wrapper 支持

bash
chmod 755 ./mvnw || true

mvnw(Maven Wrapper)是 Maven 的包装器,它允许项目携带指定版本的 Maven。chmod 755 赋予执行权限,|| true 确保即使 mvnw 不存在也不会导致构建失败。

2. 条件构建

bash
if [ -f ./mvnw ]; then ./mvnw clean package -DskipTests -T 1C; else mvn clean package -DskipTests -T 1C; fi

这段条件判断确保了:

  • 如果项目包含 Maven Wrapper,则使用 Wrapper 指定版本的 Maven 进行构建
  • 如果项目不包含 Maven Wrapper,则使用基础镜像中预装的 Maven 进行构建

3. 构建参数解析

mvn clean package -DskipTests -T 1C 命令包含以下关键参数:

参数含义说明
clean清理之前的构建产物确保构建的干净性
package打包项目生成 JAR/WAR 文件
-DskipTests跳过测试执行在 CI/CD 中,测试通常在单独的阶段执行
-T 1C每个 CPU 核心一个线程并行构建,显著加速多模块项目

-T 1C 参数详解:

Maven 的 -T 参数控制构建并行度,支持以下几种模式:

  • -T 1:使用 1 个线程(串行构建)
  • -T 1C:每个 CPU 核心使用 1 个线程(推荐)
  • -T 2C:每个 CPU 核心使用 2 个线程(激进模式)
  • -T 4:固定使用 4 个线程

对于 smart-scaffold 这类多模块项目,-T 1C 是最佳选择。以 SpringBoot 版为例,它包含 4 个模块(common、dao、service、web),使用 -T 1C 可以让 Maven 并行构建相互独立的模块。在实际测试中,-T 1C 可以将构建时间缩短 30-50%。

2.3 运行阶段详解

2.3.1 基础镜像选择:azul/zulu-openjdk:17

运行阶段使用 azul/zulu-openjdk:17 作为基础镜像,而非构建阶段的 Eclipse Temurin。这个选择基于以下考量:

Azul Zulu 的优势:

  1. 经过认证的 OpenJDK 构建: Azul Systems 是 Java 生态系统中久负盛名的公司,其 Zulu 版 OpenJDK 通过了 TCK(Technology Compatibility Kit)认证。
  2. 优秀的容器化支持: Azul 提供了专门优化的容器镜像,包括精简版(Alpine-based)和标准版(Debian-based)。
  3. 及时的安全更新: Azul 以快速发布安全补丁著称,通常在 Oracle 发布 CVE 修复后 24 小时内提供对应版本。
  4. 长期支持承诺: Azul 为每个 LTS 版本提供至少 10 年的支持周期。

镜像体积对比:

镜像体积包含内容适用场景
azul/zulu-openjdk:17~300MBJRE + 常用工具生产环境(推荐)
azul/zulu-openjdk-alpine:17~180MBJRE(精简)对体积敏感的场景
eclipse-temurin:17-jre~220MBJRE通用场景
eclipse-temurin:17-jre-alpine~150MBJRE(精简)极致体积优化

选择 azul/zulu-openjdk:17 而非 Alpine 版本的原因是:标准版基于 Debian,提供了更好的 glibc 兼容性。一些 Java 库(特别是涉及 JNI 的库)在 Alpine 的 musl libc 上可能存在兼容性问题。对于生产环境,稳定性优先于体积优化。

2.3.2 工作目录设置

dockerfile
WORKDIR /docker

将工作目录设置为 /docker 而非默认的 //app,有以下好处:

  1. 语义清晰: /docker 目录名明确表示这是 Docker 容器内的应用目录。
  2. 避免冲突: 避免与系统目录(如 /app/opt)产生冲突。
  3. 路径一致性:entrypoint.sh 中的路径保持一致。

2.4 JAR 提取策略

2.4.1 SpringBoot 版的 JAR 提取

SpringBoot 版 Dockerfile 中的 JAR 提取逻辑如下:

dockerfile
RUN \
    echo "=== 构建产物 JAR 列表 ===" \
    && ls -l /app/smart-scaffold-web/target/ \
    && mkdir -p /app/build \
    && first_jar=$(ls /app/smart-scaffold-web/target/*.jar | head -n 1) \
    && cp "$first_jar" /app/build/app.jar \
    && echo "=== 复制后的应用 JAR ===" \
    && ls -l /app/build/

这段代码的执行流程:

  1. 列出构建产物: ls -l /app/smart-scaffold-web/target/ 打印构建产物列表,便于调试。
  2. 创建输出目录: mkdir -p /app/build 确保输出目录存在。
  3. 提取 JAR 文件: first_jar=$(ls /app/smart-scaffold-web/target/*.jar | head -n 1) 获取第一个匹配的 JAR 文件。

注意: 这里使用 head -n 1 而不是 grep -v originalspring-boot-maven-pluginrepackage 目标会生成两个文件:

  • smart-scaffold-web-1.0.0-SNAPSHOT.jar:可执行的 Fat JAR(包含所有依赖)
  • smart-scaffold-web-1.0.0-SNAPSHOT.jar.original:原始的瘦 JAR(仅包含应用代码)

使用 head -n 1 时,取决于文件系统的排序顺序,可能会选择到 .original 文件。在实际项目中,建议使用更精确的提取逻辑。

2.4.2 Dubbo/SpringCloud 版的 JAR 提取

Dubbo 和 SpringCloud 版的 Dockerfile 使用了更精确的 JAR 提取策略:

dockerfile
RUN echo "=== 复制 provider jar ===" \
    && mkdir -p /app/build \
    && jar=$(ls /app/smart-scaffold-provider/target/*.jar | grep -v original | head -n 1) \
    && cp "$jar" /app/build/app.jar

关键区别在于 grep -v original,它明确排除了 .original 后缀的文件,确保只提取可执行的 Fat JAR。

两种提取策略的对比:

策略命令准确性推荐度
取第一个head -n 1不确定不推荐
排除 originalgrep -v original | head -n 1推荐
精确匹配ls target/*-SNAPSHOT.jar | grep -v original最高最佳

第三章 SpringBoot 版 Dockerfile 深度解析

3.1 项目结构分析

smart-scaffold-springboot 是一个典型的 Spring Boot 分层单体项目,其模块依赖关系如下:

smart-scaffold-springboot (父 POM)

├── smart-scaffold-common     [无外部依赖]
│   └── 工具类、常量、通用模型

├── smart-scaffold-dao        [依赖 common]
│   └── MyBatis Mapper、数据访问层

├── smart-scaffold-service    [依赖 dao]
│   └── 业务逻辑层、事务管理

└── smart-scaffold-web        [依赖 service]
    └── Controller、配置、启动类
    └── Spring Boot 可执行 JAR (最终产物)

模块依赖链: web -> service -> dao -> common

这意味着 Maven 构建时必须按照依赖顺序编译:先编译 common,再编译 dao,然后是 service,最后是 web。Maven 的 Reactor 机制会自动处理这种依赖关系。

关键 POM 配置:

xml
<!-- 父 POM 中的模块声明 -->
<modules>
    <module>smart-scaffold-common</module>
    <module>smart-scaffold-dao</module>
    <module>smart-scaffold-service</module>
    <module>smart-scaffold-web</module>
</modules>

<!-- web 模块中的 Spring Boot 打包插件 -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>cc.bima.scaffold.ScaffoldWebApplication</mainClass>
        <includeSystemScope>true</includeSystemScope>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

spring-boot-maven-pluginrepackage 目标是关键——它将普通的 JAR 文件重新打包为可执行的 Fat JAR。执行 repackage 后,target/ 目录下会生成两个文件:

  • smart-scaffold-web-1.0.0-SNAPSHOT.jar:Fat JAR(约 100MB)
  • smart-scaffold-web-1.0.0-SNAPSHOT.jar.original:原始 JAR(约 5MB)

3.2 完整 Dockerfile 逐行解析

以下是 smart-scaffold-springboot 的完整 Dockerfile,逐行进行深度解析:

dockerfile
# ============================================================
# 阶段1:构建阶段
# 目标:在 Maven 环境中编译源代码,生成可执行 Fat JAR
# ============================================================
FROM maven:3.9.9-eclipse-temurin-17 AS builder

解析: 使用 maven:3.9.9-eclipse-temurin-17 作为构建阶段的基础镜像。AS builder 为该阶段命名为 builder,后续阶段可以通过 COPY --from=builder 引用此阶段的产物。

dockerfile
WORKDIR /app

解析: 设置工作目录为 /app。后续的 COPYRUNCMD 等指令都以此目录为基准。

dockerfile
# 优化 Maven 构建配置
RUN mkdir -p ~/.m2 \
    && echo "<settings><localRepository>/tmp/m2repo</localRepository><offline>false</offline><mirrors></mirrors></settings>" > ~/.m2/settings.xml

解析: 配置 Maven 的本地仓库路径为 /tmp/m2repo。使用 && 连接多个命令以减少镜像层数。

dockerfile
# 复制项目文件
COPY . .

解析: 将构建上下文中的所有文件复制到容器的 /app 目录。

优化建议: 对于大型项目,可以利用 Docker 的层缓存机制,将依赖解析和源代码编译分离:

dockerfile
# 优化版:分离依赖解析和源代码编译
COPY pom.xml .
COPY smart-scaffold-common/pom.xml smart-scaffold-common/
COPY smart-scaffold-dao/pom.xml smart-scaffold-dao/
COPY smart-scaffold-service/pom.xml smart-scaffold-service/
COPY smart-scaffold-web/pom.xml smart-scaffold-web/

# 下载依赖(这一层会被缓存,除非 POM 变化)
RUN mvn dependency:go-offline -B

# 再复制源代码
COPY . .

# 编译打包
RUN mvn clean package -DskipTests -T 1C
dockerfile
# 构建:生成 Spring Boot 可执行 JAR
RUN chmod 755 ./mvnw || true \
    && (if [ -f ./mvnw ]; then ./mvnw clean package -DskipTests -T 1C; else mvn clean package -DskipTests -T 1C; fi)

解析: 执行 Maven 构建。优先使用 Maven Wrapper(如果存在),否则使用系统安装的 Maven。

dockerfile
# 复制构建产物
RUN \
    echo "=== 构建产物 JAR 列表 ===" \
    && ls -l /app/smart-scaffold-web/target/ \
    && mkdir -p /app/build \
    && first_jar=$(ls /app/smart-scaffold-web/target/*.jar | head -n 1) \
    && cp "$first_jar" /app/build/app.jar \
    && echo "=== 复制后的应用 JAR ===" \
    && ls -l /app/build/

解析: 从构建输出目录中提取 JAR 文件,复制到统一的 /app/build/ 目录。

dockerfile
# ============================================================
# 阶段2:运行阶段
# ============================================================
FROM azul/zulu-openjdk:17 AS runner
WORKDIR /docker

COPY --from=builder /app/build/app.jar /docker/app.jar
COPY entrypoint.sh /docker/entrypoint.sh

RUN chmod +x /docker/entrypoint.sh
ENV PATH $PATH:$JAVA_HOME/bin:/docker

EXPOSE 8080
ENTRYPOINT ["/docker/entrypoint.sh"]

解析: 切换到全新的基础镜像,只复制构建产物和启动脚本。ENTRYPOINT 使用 exec 格式,确保 Java 进程作为 PID 1 运行。

3.3 构建优化策略

3.3.1 依赖缓存优化

在 CI/CD 场景中,Maven 依赖下载往往是构建中最耗时的环节。通过 Docker 层缓存,我们可以将依赖下载与源代码编译分离:

性能对比:

场景优化前优化后提升
首次构建5 分钟5 分钟无变化
仅修改源代码5 分钟2 分钟60%
仅修改 POM5 分钟5 分钟无变化

3.3.2 Maven 并行构建优化

-T 1C 参数是 Maven 并行构建的核心。在 smart-scaffold-springboot 的 4 模块结构中,并行构建将总构建时间从约 120 秒缩短到约 75 秒,提升约 37%。

3.3.3 .dockerignore 优化

为 smart-scaffold-springboot 项目推荐的 .dockerignore 配置:

dockerignore
.git
.gitignore
.idea
*.iml
.settings
.project
.classpath
target/
build/
*.md
LICENSE.txt
README.md
.DS_Store
Thumbs.db
*.sql
*.log

第四章 Dubbo/SpringCloud 版 Dockerfile 深度解析

4.1 Dubbo 版项目结构

smart-scaffold-dubbo 采用 Apache Dubbo RPC 框架,项目结构如下:

smart-scaffold-dubbo/
├── smart-scaffold-api/              # API 接口定义模块
├── smart-scaffold-provider/         # 服务提供者(可部署单元)
│   ├── Dockerfile
│   └── entrypoint.sh
├── smart-scaffold-consumer/         # 服务消费者(可部署单元)
│   ├── Dockerfile
│   └── entrypoint.sh
└── pom.xml

模块依赖关系:

smart-scaffold-api (接口定义)
    |
    ├── smart-scaffold-provider (实现接口)
    └── smart-scaffold-consumer (调用接口)

Provider 模块的核心依赖包括 Dubbo RPC 框架(3.2.0)、Zookeeper 注册中心(3.9.1)和 Curator 客户端封装(5.1.0)。

4.2 SpringCloud 版项目结构

smart-scaffold-springcloud 采用 Spring Cloud 微服务框架,项目结构与 Dubbo 版类似。关键依赖差异在于使用 OpenFeign 替代 Dubbo 进行服务间调用,使用 Spring Cloud LoadBalancer 进行负载均衡。

Dubbo vs SpringCloud 容器化差异:

维度Dubbo 版SpringCloud 版
通信协议Dubbo 协议(TCP 长连接)HTTP/REST(短连接)
服务发现ZookeeperZookeeper
负载均衡Dubbo 内置Spring Cloud LoadBalancer
端口暴露Provider: 20880, Consumer: 8080Provider: 8080, Consumer: 8081

4.3 Provider Dockerfile 解析

以下是 Dubbo 版 Provider 的完整 Dockerfile:

dockerfile
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY . .

RUN echo "=== 复制 provider jar ===" \
    && mkdir -p /app/build \
    && jar=$(ls /app/smart-scaffold-provider/target/*.jar | grep -v original | head -n 1) \
    && cp "$jar" /app/build/app.jar

FROM azul/zulu-openjdk:17
WORKDIR /docker
COPY --from=builder /app/build/app.jar app.jar
COPY ./smart-scaffold-provider/entrypoint.sh .
RUN chmod +x entrypoint.sh

ENTRYPOINT ["/docker/entrypoint.sh"]

与 SpringBoot 版 Dockerfile 的关键差异:

  1. 没有 Maven 构建步骤: Provider Dockerfile 中没有 mvn clean package 命令。它假设 JAR 文件已经通过外部构建生成。
  2. 从根目录 COPY 全部源码: COPY . . 将整个项目(包括所有子模块)复制到构建阶段。
  3. 精确的 JAR 提取: 使用 grep -v original 确保只提取可执行的 Fat JAR。

4.4 Consumer Dockerfile 解析

Consumer 的 Dockerfile 与 Provider 几乎完全相同,唯一的区别是 JAR 文件的路径:

dockerfile
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY . .

RUN echo "=== 复制 consumer jar ===" \
    && mkdir -p /app/build \
    && jar=$(ls /app/smart-scaffold-consumer/target/*.jar | grep -v original | head -n 1) \
    && cp "$jar" /app/build/app.jar

FROM azul/zulu-openjdk:17
WORKDIR /docker
COPY --from=builder /app/build/app.jar app.jar
COPY ./smart-scaffold-consumer/entrypoint.sh .
RUN chmod +x entrypoint.sh

ENTRYPOINT ["/docker/entrypoint.sh"]

4.5 预构建模式分析

Dubbo/SpringCloud 版采用"预构建模式",即 Dockerfile 假设 JAR 文件已经构建完成。这种模式在 CI/CD 流水线中的工作流程如下:

+----------------------------------------------------------+
|                    CI/CD 流水线                             |
|                                                          |
|  步骤1: Maven 统一构建                                    |
|  +--------------------------------------------------+    |
|  | mvn clean package -DskipTests -T 1C              |    |
|  | -> api/target/*.jar                               |    |
|  | -> provider/target/*.jar (Fat JAR)                |    |
|  | -> consumer/target/*.jar (Fat JAR)                |    |
|  +--------------------------------------------------+    |
|                         |                                |
|           +-------------+-------------+                  |
|           |                           |                  |
|           v                           v                  |
|  步骤2a: 构建 Provider 镜像    步骤2b: 构建 Consumer 镜像 |
|           |                           |                  |
|           +-------------+-------------+                  |
|                         v                                |
|  步骤3: 推送镜像到 Registry                               |
|                         |                                |
|                         v                                |
|  步骤4: SSH 远程部署                                      |
+----------------------------------------------------------+

预构建模式的优势:

  1. 构建一次,多处使用: Maven 只需执行一次,所有模块的 JAR 都会被构建。
  2. 并行镜像构建: Provider 和 Consumer 的 Docker 镜像可以并行构建。
  3. 构建缓存效率高: Maven 构建结果可以被 Docker 镜像构建直接使用。

4.6 子模块 JAR 提取策略

在多模块项目中,正确提取目标子模块的 JAR 文件至关重要。以下是三种提取策略:

策略一:排除 original 后缀(当前使用)

bash
jar=$(ls /app/smart-scaffold-provider/target/*.jar | grep -v original | head -n 1)

策略二:精确匹配文件名(推荐)

bash
jar="/app/smart-scaffold-provider/target/smart-scaffold-provider-1.0.0-SNAPSHOT.jar"
if [ ! -f "$jar" ]; then
    jar=$(ls /app/smart-scaffold-provider/target/*.jar | grep -v original | head -n 1)
fi

策略三:使用 Maven Help Plugin 查询

bash
JAR_PATH=$(mvn help:evaluate -Dexpression=project.build.finalName -q -DforceStdout)
jar="/app/smart-scaffold-provider/target/${JAR_PATH}.jar"

第五章 entrypoint.sh 启动脚本分析

5.1 基础启动脚本

smart-scaffold 项目的 entrypoint.sh 脚本非常简洁:

bash
#!/bin/bash

# 启动 Spring Boot 应用
java -jar /docker/app.jar

虽然只有两行代码,但这个脚本承载了几个重要的职责:

  1. 标准化启动入口: 无论底层使用什么框架,启动方式统一为 java -jar
  2. Shell 脚本灵活性: 使用 Shell 脚本而非直接在 Dockerfile 中写 CMD,可以方便地添加环境检查、参数注入等逻辑。
  3. 信号处理: Bash 脚本会自动将接收到的信号传递给子进程(Java 进程),确保 docker stop 能够优雅地关闭应用。

为什么使用 ENTRYPOINT 而非 CMD?

ENTRYPOINT 定义容器启动时执行的命令,CMD 提供默认参数。使用 ENTRYPOINT 时,docker run 的命令行参数会追加到后面,而不是替换它:

bash
# 使用 ENTRYPOINT 时,可以传递额外参数
docker run myapp --spring.profiles.active=prod
# 等效于执行:/docker/entrypoint.sh --spring.profiles.active=prod

5.2 JVM 参数配置最佳实践

5.2.1 容器化环境下的 JVM 参数原则

原则1:永远不要将堆内存设置为容器内存限制的 100%

bash
# 错误示例:容器限制 1GB,堆内存设置为 1GB
java -Xmx1g -jar app.jar  # 容器会被 OOM Killer 杀掉

# 正确示例:容器限制 1GB,堆内存设置为 75%
java -Xmx768m -jar app.jar

内存分配建议:

容器内存堆内存 (-Xmx)元空间直接内存线程栈缓冲
512MB384MB64MB32MB~32MB~0MB
1GB768MB128MB64MB~32MB~8MB
2GB1536MB256MB128MB~64MB~16MB
4GB3072MB256MB256MB~128MB~288MB

原则2:使用 G1GC 作为默认垃圾收集器

从 Java 9 开始,G1GC 已成为默认的垃圾收集器。对于容器化环境,G1GC 的优势尤为明显:可预测的停顿时间、适合容器化、内存碎片少。

原则3:启用容器感知

bash
java -XX:+UseContainerSupport -jar /docker/app.jar

Java 17 默认已启用容器感知,但显式声明更安全。

5.2.2 生产级 JVM 参数模板

bash
#!/bin/bash

JAVA_OPTS="${JAVA_OPTS:-}"
JAVA_OPTS="$JAVA_OPTS -Xms${HEAP_SIZE:-512m}"
JAVA_OPTS="$JAVA_OPTS -Xmx${HEAP_MAX_SIZE:-768m}"
JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=${METASPACE_SIZE:-256m}"
JAVA_OPTS="$JAVA_OPTS -XX:MaxDirectMemorySize=${DIRECT_MEMORY:-128m}"
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200"
JAVA_OPTS="$JAVA_OPTS -XX:G1HeapRegionSize=8m"
JAVA_OPTS="$JAVA_OPTS -XX:InitiatingHeapOccupancyPercent=35"
JAVA_OPTS="$JAVA_OPTS -XX:+UseContainerSupport"
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:stdout:time,uptime:filecount=5,filesize=20m"
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/docker/logs/heapdump.hprof"
JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom"

exec java $JAVA_OPTS ${SPRING_OPTS:-} -jar /docker/app.jar

参数说明:

参数作用推荐值
-Xms初始堆大小与 -Xmx 相同
-Xmx最大堆大小容器内存的 75%
-XX:MaxMetaspaceSize元空间上限256MB
-XX:MaxDirectMemorySize直接内存上限128MB
-XX:+UseG1GC使用 G1 垃圾收集器开启
-XX:MaxGCPauseMillis最大 GC 停顿时间目标200ms
-Xlog:gc*GC 日志输出stdout
-XX:+HeapDumpOnOutOfMemoryErrorOOM 时自动导出堆转储开启

5.3 环境变量传递机制

Docker 提供了多种方式将环境变量传递给容器内的应用:

方式一:docker run -e

bash
docker run -d \
    -e JAVA_OPTS="-Xms512m -Xmx768m" \
    -e SPRING_PROFILES_ACTIVE=prod \
    myapp:latest

方式二:--env-file

bash
docker run -d --env-file env.production myapp:latest

方式三:Docker Compose

yaml
services:
  app:
    environment:
      - JAVA_OPTS=-Xms512m -Xmx768m
    env_file:
      - .env.production

方式四:Spring Boot 配置覆盖

Spring Boot 支持通过环境变量覆盖配置文件中的任何属性:

bash
SPRING_DATASOURCE_URL=jdbc:mysql://prod-db:3306/mydb
SPRING_DATASOURCE_USERNAME=prod_user
SPRING_PROFILES_ACTIVE=prod

5.4 生产级增强版 entrypoint.sh

bash
#!/bin/bash
set -e

LOG_PREFIX="[entrypoint]"

echo "$LOG_PREFIX ============================================"
echo "$LOG_PREFIX smart-scaffold 应用启动"
echo "$LOG_PREFIX 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "$LOG_PREFIX Java 版本: $(java -version 2>&1 | head -n 1)"
echo "$LOG_PREFIX ============================================"

# 1. 环境预检查
if [ ! -f /docker/app.jar ]; then
    echo "$LOG_PREFIX 错误: /docker/app.jar 不存在"
    exit 1
fi

JAR_SIZE=$(stat -c%s /docker/app.jar 2>/dev/null || echo "0")
if [ "$JAR_SIZE" -lt 1000 ]; then
    echo "$LOG_PREFIX 错误: app.jar 文件过小 ($JAR_SIZE bytes)"
    exit 1
fi

# 2. JVM 参数配置
HEAP_SIZE="${HEAP_SIZE:-512m}"
HEAP_MAX_SIZE="${HEAP_MAX_SIZE:-768m}"

JAVA_OPTS="${JAVA_OPTS:-}"
JAVA_OPTS="$JAVA_OPTS -Xms${HEAP_SIZE}"
JAVA_OPTS="$JAVA_OPTS -Xmx${HEAP_MAX_SIZE}"
JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=256m"
JAVA_OPTS="$JAVA_OPTS -XX:MaxDirectMemorySize=128m"
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200"
JAVA_OPTS="$JAVA_OPTS -XX:+UseContainerSupport"
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:stdout:time,uptime:filecount=5,filesize=20m"
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/docker/logs/"
JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom"

# 3. Spring Boot 参数
SPRING_OPTS="${SPRING_OPTS:-}"
if [ -n "$SPRING_PROFILES_ACTIVE" ]; then
    SPRING_OPTS="$SPRING_OPTS --spring.profiles.active=$SPRING_PROFILES_ACTIVE"
fi

# 4. 创建日志目录
mkdir -p /docker/logs

# 5. 打印启动信息
echo "$LOG_PREFIX JVM 参数: $JAVA_OPTS"
echo "$LOG_PREFIX Spring 参数: $SPRING_OPTS"
echo "$LOG_PREFIX 开始启动应用..."

# 6. 启动应用(使用 exec 确保信号传递)
exec java $JAVA_OPTS $SPRING_OPTS -jar /docker/app.jar

关键设计要点:

  1. set -e 任何命令失败时立即退出。
  2. exec java 使用 exec 替换当前 Shell 进程,使 Java 进程成为容器的 PID 1。
  3. 环境预检查: 启动前检查 JAR 文件是否存在且完整。
  4. 参数分层: 将 JVM 参数和 Spring 参数分离,便于独立配置。

第六章 Jenkins CI/CD 流水线

6.1 Jenkins 流水线架构设计

smart-scaffold 项目的 CI/CD 流水线采用 Jenkins 作为核心自动化工具,整体架构如下:

+-------------------------------------------------------------+
|                     Jenkins CI/CD 流水线                      |
|                                                             |
|  +----------+   +----------+   +----------+   +---------+  |
|  | 代码检出  |-->| Maven    |-->| Docker   |-->| SSH     |  |
|  | (Git)    |   | 构建     |   | 镜像构建  |   | 远程部署 |  |
|  +----------+   +----------+   +----------+   +---------+  |
|       |              |              |              |        |
|       v              v              v              v        |
|  源代码         编译后的       Docker 镜像     目标服务器    |
|  仓库           JAR 文件       (Registry)     运行容器      |
+-------------------------------------------------------------+

流水线阶段详解:

  1. 代码检出(Checkout): 从 Git 仓库拉取最新代码。
  2. Maven 构建(Build): 编译源代码,运行单元测试,生成 JAR 文件。
  3. Docker 镜像构建(Package): 基于 Dockerfile 构建应用镜像。
  4. SSH 远程部署(Deploy): 通过 SSH 连接目标服务器,拉取镜像并启动容器。

6.2 Build Steps 配置

6.2.1 SpringBoot 版 Build Steps

Build Step 1:Maven 构建

构建步骤类型: Invoke top-level Maven targets
Maven 版本: Maven 3.9
目标 (Goals): clean package -DskipTests -T 1C
POM: pom.xml

Build Step 2:Docker 镜像构建

构建步骤类型: Execute shell
命令:
    docker build -t smart-scaffold-springboot:latest .
    docker tag smart-scaffold-springboot:latest \
        registry.example.com/smart-scaffold-springboot:${BUILD_NUMBER}

Build Step 3:SSH 远程部署

构建步骤类型: Send build artifacts over SSH
执行命令:
    docker pull registry.example.com/smart-scaffold-springboot:latest
    docker stop smart-scaffold-springboot || true
    docker rm smart-scaffold-springboot || true
    docker run -d \
        --name smart-scaffold-springboot \
        --restart=always \
        -p 8080:8080 \
        -e SPRING_PROFILES_ACTIVE=prod \
        registry.example.com/smart-scaffold-springboot:latest

6.2.2 Dubbo/SpringCloud 版 Build Steps

Dubbo 和 SpringCloud 版需要构建和部署多个服务。关键区别在于需要分别构建 Provider 和 Consumer 的 Docker 镜像,并在部署时按顺序启动。

6.3 Maven 构建阶段

在 Jenkins 全局配置中,需要正确配置 Maven 和 settings.xml。推荐在 Jenkins 服务器的 settings.xml 中配置阿里云镜像加速:

xml
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
    <localRepository>/var/jenkins_home/.m2/repository</localRepository>
    <mirrors>
        <mirror>
            <id>aliyunmaven</id>
            <mirrorOf>*</mirrorOf>
            <name>阿里云公共仓库</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </mirror>
    </mirrors>
</settings>

6.4 Docker 镜像构建阶段

在 Jenkins Build Step 中,Docker 镜像构建通常使用 Shell 脚本执行:

bash
#!/bin/bash
set -e

IMAGE_NAME="smart-scaffold-springboot"
REGISTRY="registry.example.com"
TAG="${BUILD_NUMBER:-latest}"

docker build \
    --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
    --build-arg VCS_REF=$(git rev-parse --short HEAD) \
    -t ${REGISTRY}/${IMAGE_NAME}:${TAG} \
    -t ${REGISTRY}/${IMAGE_NAME}:latest \
    .

推荐镜像标签策略:

标签类型格式用途保留策略
构建号42精确版本追踪永久保留
日期20260409按日期回滚保留 30 天
Git 哈希abc1234代码版本追溯永久保留
latestlatest默认拉取自动更新

6.5 SSH 远程部署

在 Jenkins 中配置 SSH 远程部署有两种方式:

方式一:Jenkins SSH 插件

通过 Jenkins Web 界面配置 SSH Server,使用 Publish Over SSH 插件执行远程命令。

方式二:Shell 脚本直接 SSH

bash
#!/bin/bash
set -e

DEPLOY_HOST="deploy@192.168.1.100"

ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} << 'DEPLOY_SCRIPT'
    set -e
    cd /opt/smart-scaffold
    docker pull registry.example.com/smart-scaffold-springboot:latest
    docker stop smart-scaffold-springboot || true
    docker rm smart-scaffold-springboot || true
    docker run -d \
        --name smart-scaffold-springboot \
        --restart=always \
        -p 8080:8080 \
        --env-file .env \
        registry.example.com/smart-scaffold-springboot:latest
DEPLOY_SCRIPT

6.6 Docker 容器生命周期管理

在部署脚本中,容器的生命周期管理遵循以下标准流程:

bash
# 1. 停止旧容器(优雅关闭,等待 10 秒)
docker stop -t 10 smart-scaffold-springboot || true

# 2. 删除旧容器
docker rm smart-scaffold-springboot || true

# 3. 拉取最新镜像
docker pull registry.example.com/smart-scaffold-springboot:latest

# 4. 启动新容器
docker run -d \
    --name smart-scaffold-springboot \
    --restart=always \
    -p 8080:8080 \
    registry.example.com/smart-scaffold-springboot:latest

# 5. 验证容器状态
docker ps | grep smart-scaffold-springboot
docker logs --tail 50 smart-scaffold-springboot

docker stop -t 10 的含义: 发送 SIGTERM 后等待 10 秒。如果应用在 10 秒内未退出,Docker 会发送 SIGKILL 强制终止。Spring Boot 应用收到 SIGTERM 后会触发优雅关闭。

6.7 --restart=always 自愈策略

Docker 提供了四种容器重启策略:

策略行为适用场景
no不自动重启(默认)一次性任务
on-failure[:max-retries]非正常退出时重启批处理任务
unless-stopped总是重启,除非手动停止一般服务
always总是重启生产服务(推荐)

smart-scaffold 项目使用 --restart=always 策略,确保服务在异常退出后自动恢复。

自愈场景分析:

  • 应用崩溃: OOM 或未捕获异常导致容器退出后,Docker 会在 1-2 秒内自动重启容器。
  • Docker daemon 重启: daemon 恢复后自动启动所有 --restart=always 的容器。
  • 服务器重启: Docker 服务自动启动后,容器随之启动。

增强方案: 结合健康检查使用:

bash
docker run -d \
    --name smart-scaffold-springboot \
    --restart=always \
    --health-cmd="curl -f http://localhost:8080/actuator/health || exit 1" \
    --health-interval=30s \
    --health-timeout=5s \
    --health-retries=3 \
    --health-start-period=60s \
    -p 8080:8080 \
    smart-scaffold-springboot:latest

6.8 多服务编排部署脚本

以下是 Dubbo 版的完整部署脚本示例:

bash
#!/bin/bash
set -e

REGISTRY="registry.example.com"
ENV_FILE="/opt/smart-scaffold-dubbo/.env"

echo "=== smart-scaffold-dubbo 部署开始 ==="

# 1. 部署 Provider
echo "--- 部署 Provider ---"
docker pull ${REGISTRY}/smart-scaffold-provider:latest
docker stop smart-scaffold-provider || true
docker rm smart-scaffold-provider || true

docker run -d \
    --name smart-scaffold-provider \
    --restart=always \
    --network scaffold-net \
    -p 20880:20880 \
    -p 8081:8081 \
    --env-file ${ENV_FILE} \
    -e SPRING_PROFILES_ACTIVE=prod \
    ${REGISTRY}/smart-scaffold-provider:latest

# 2. 等待 Provider 就绪
echo "--- 等待 Provider 就绪 ---"
for i in $(seq 1 30); do
    if docker exec smart-scaffold-provider \
        curl -sf http://localhost:8081/actuator/health > /dev/null 2>&1; then
        echo "Provider 已就绪"
        break
    fi
    sleep 5
done

# 3. 部署 Consumer
echo "--- 部署 Consumer ---"
docker pull ${REGISTRY}/smart-scaffold-consumer:latest
docker stop smart-scaffold-consumer || true
docker rm smart-scaffold-consumer || true

docker run -d \
    --name smart-scaffold-consumer \
    --restart=always \
    --network scaffold-net \
    -p 8080:8080 \
    --env-file ${ENV_FILE} \
    -e SPRING_PROFILES_ACTIVE=prod \
    ${REGISTRY}/smart-scaffold-consumer:latest

echo "=== 部署完成 ==="
docker ps --filter network=scaffold-net

Jenkins Pipeline 版本:

groovy
pipeline {
    agent any
    environment {
        REGISTRY = 'registry.example.com'
    }
    stages {
        stage('Checkout') {
            steps { git 'https://github.com/your-org/smart-scaffold-dubbo.git' }
        }
        stage('Maven Build') {
            steps { sh 'mvn clean package -DskipTests -T 1C' }
        }
        stage('Build Images') {
            steps {
                sh 'cd smart-scaffold-provider && docker build -t ${REGISTRY}/smart-scaffold-provider:${BUILD_NUMBER} .'
                sh 'cd smart-scaffold-consumer && docker build -t ${REGISTRY}/smart-scaffold-consumer:${BUILD_NUMBER} .'
            }
        }
        stage('Deploy') {
            steps {
                sh 'ssh deploy@production-server "cd /opt/smart-scaffold-dubbo && ./deploy.sh"'
            }
        }
    }
}

第七章 Maven 阿里云镜像加速

7.1 settings.xml 配置详解

7.1.1 为什么需要镜像加速

Maven Central 位于海外服务器,在中国大陆下载速度通常在 10-50 KB/s。阿里云 Maven 镜像在国内拥有优秀的网络质量和 CDN 加速,下载速度通常可以达到 5-20 MB/s。

7.1.2 完整 settings.xml 配置

xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0
          https://maven.apache.org/xsd/settings-1.2.0.xsd">

    <localRepository>${user.home}/.m2/repository</localRepository>

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

    <profiles>
        <profile>
            <id>aliyun</id>
            <repositories>
                <repository>
                    <id>aliyun-central</id>
                    <url>https://maven.aliyun.com/repository/central</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </repository>
                <repository>
                    <id>aliyun-apache-snapshots</id>
                    <url>https://maven.aliyun.com/repository/apache-snapshots</url>
                    <releases><enabled>false</enabled></releases>
                    <snapshots><enabled>true</enabled></snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>aliyun-plugin</id>
                    <url>https://maven.aliyun.com/repository/public</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>

    <activeProfiles>
        <activeProfile>aliyun</activeProfile>
    </activeProfiles>
</settings>

7.1.3 配置文件位置

位置路径作用范围适用场景
全局配置${maven.home}/conf/settings.xml所有用户Jenkins 服务器
用户配置${user.home}/.m2/settings.xml当前用户开发环境
项目配置.mvn/maven.config当前项目CI/CD 环境

7.2 镜像地址选择

阿里云 Maven 镜像提供了多个仓库:

仓库URL包含内容适用场景
centralhttps://maven.aliyun.com/repository/centralMaven Central通用依赖
publichttps://maven.aliyun.com/repository/publicCentral + JCenter + Google推荐(聚合仓库)
apache-snapshotshttps://maven.aliyun.com/repository/apache-snapshotsApache 快照版快照依赖
springhttps://maven.aliyun.com/repository/springSpring 系列依赖Spring 项目

推荐配置: 使用 public 仓库作为统一镜像(<mirrorOf>*</mirrorOf>),它聚合了多个仓库的内容。

7.3 构建速度优化

7.3.1 依赖解析缓存

通过配置 updatePolicy 可以控制 Maven 检查依赖更新的频率:

含义适用场景
always每次构建都检查更新快照版本(推荐)
daily每天检查一次(默认)日常开发
never从不检查更新稳定的 Release 版本

7.3.2 Docker 构建中的 Maven 缓存

方案一:使用 BuildKit 缓存挂载(推荐)

dockerfile
# syntax=docker/dockerfile:1
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /app

RUN --mount=type=cache,target=/root/.m2/repository \
    mvn clean package -DskipTests -T 1C

方案二:多阶段构建 + 依赖层缓存

dockerfile
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /app

# 先复制 POM 文件
COPY pom.xml .
COPY smart-scaffold-common/pom.xml smart-scaffold-common/
COPY smart-scaffold-dao/pom.xml smart-scaffold-dao/
COPY smart-scaffold-service/pom.xml smart-scaffold-service/
COPY smart-scaffold-web/pom.xml smart-scaffold-web/

# 下载依赖(这一层会被缓存)
RUN --mount=type=cache,target=/root/.m2/repository \
    mvn dependency:go-offline -B

# 再复制源代码并编译
COPY . .
RUN --mount=type=cache,target=/root/.m2/repository \
    mvn clean package -DskipTests -T 1C

性能对比:

方案首次构建增量构建实现复杂度
无缓存5 分钟5 分钟
BuildKit 缓存5 分钟1.5 分钟
依赖层缓存5 分钟1 分钟

第八章 Docker Compose 开发环境

8.1 多服务编排

Docker Compose 通过一个 YAML 文件定义和管理多容器应用,提供声明式配置、一键启动、依赖管理、网络管理和数据持久化等能力。

8.2 网络配置

Docker 提供了多种网络模式。推荐使用自定义网络,因为它提供了内置的 DNS 解析功能,容器可以通过服务名互相访问:

yaml
services:
  provider:
    networks:
      - scaffold-net
  consumer:
    networks:
      - scaffold-net
    # Consumer 可以通过 http://provider:8080 访问 Provider

networks:
  scaffold-net:
    driver: bridge

8.3 数据持久化

方式语法数据位置适用场景
Volumedb-data:/var/lib/mysqlDocker 管理数据库数据(推荐)
Bind Mount./data:/var/lib/mysql宿主机路径配置文件、日志

8.4 环境变量管理

推荐做法:将敏感信息(密码、密钥)放在 .env 文件中(加入 .gitignore),将非敏感配置放在 docker-compose.yml 中。

8.5 SpringBoot 版完整 Compose 配置

yaml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: smart-scaffold-springboot
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/smart_scaffold?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=123456
      - SPRING_REDIS_HOST=redis
      - SPRING_REDIS_PORT=6379
    volumes:
      - app-logs:/docker/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - scaffold-net
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    container_name: scaffold-mysql
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=123456
      - MYSQL_DATABASE=smart_scaffold
      - MYSQL_CHARACTER_SET_SERVER=utf8mb4
      - MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci
      - TZ=Asia/Shanghai
    volumes:
      - mysql-data:/var/lib/mysql
      - ./smart-scaffold.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - scaffold-net
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: scaffold-redis
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - scaffold-net
    restart: unless-stopped

  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: scaffold-rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "check_running"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - scaffold-net
    restart: unless-stopped

networks:
  scaffold-net:
    driver: bridge

volumes:
  mysql-data:
  redis-data:
  rabbitmq-data:
  app-logs:

8.6 Dubbo 版完整 Compose 配置

yaml
version: '3.8'

services:
  zookeeper:
    image: zookeeper:3.9
    container_name: scaffold-zookeeper
    ports:
      - "2181:2181"
    environment:
      - ZOO_MY_ID=1
      - ZOO_SERVERS=server.1=0.0.0.0:2888:3888;2181
    volumes:
      - zookeeper-data:/data
    healthcheck:
      test: ["CMD", "zkServer.sh", "status"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    networks:
      - scaffold-net
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    container_name: scaffold-mysql
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=123456
      - MYSQL_DATABASE=smart_scaffold
      - TZ=Asia/Shanghai
    volumes:
      - mysql-data:/var/lib/mysql
      - ./smart-scaffold.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - scaffold-net
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: scaffold-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - scaffold-net
    restart: unless-stopped

  provider:
    build:
      context: .
      dockerfile: smart-scaffold-provider/Dockerfile
    container_name: scaffold-dubbo-provider
    ports:
      - "20880:20880"
      - "8081:8081"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - DUBBO_REGISTRY_ADDRESS=zookeeper://zookeeper:2181
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/smart_scaffold
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=123456
    depends_on:
      zookeeper:
        condition: service_healthy
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - scaffold-net
    restart: unless-stopped

  consumer:
    build:
      context: .
      dockerfile: smart-scaffold-consumer/Dockerfile
    container_name: scaffold-dubbo-consumer
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - DUBBO_REGISTRY_ADDRESS=zookeeper://zookeeper:2181
    depends_on:
      - provider
    networks:
      - scaffold-net
    restart: unless-stopped

networks:
  scaffold-net:
    driver: bridge

volumes:
  zookeeper-data:
  mysql-data:
  redis-data:

8.7 SpringCloud 版完整 Compose 配置

yaml
version: '3.8'

services:
  zookeeper:
    image: zookeeper:3.9
    container_name: scaffold-cloud-zookeeper
    ports:
      - "2181:2181"
    volumes:
      - zk-data:/data
    healthcheck:
      test: ["CMD", "zkServer.sh", "status"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - scaffold-net
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    container_name: scaffold-cloud-mysql
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=123456
      - MYSQL_DATABASE=smart_scaffold
      - TZ=Asia/Shanghai
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - scaffold-net
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: scaffold-cloud-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - scaffold-net
    restart: unless-stopped

  provider:
    build:
      context: .
      dockerfile: smart-scaffold-provider/Dockerfile
    container_name: scaffold-cloud-provider
    ports:
      - "8081:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_CLOUD_ZOOKEEPER_CONNECT_STRING=zookeeper:2181
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/smart_scaffold
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=123456
    depends_on:
      zookeeper:
        condition: service_healthy
      mysql:
        condition: service_healthy
    networks:
      - scaffold-net
    restart: unless-stopped

  consumer:
    build:
      context: .
      dockerfile: smart-scaffold-consumer/Dockerfile
    container_name: scaffold-cloud-consumer
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_CLOUD_ZOOKEEPER_CONNECT_STRING=zookeeper:2181
    depends_on:
      - provider
    networks:
      - scaffold-net
    restart: unless-stopped

networks:
  scaffold-net:
    driver: bridge

volumes:
  zk-data:
  mysql-data:
  redis-data:

SpringCloud 版与 Dubbo 版的主要差异:

  1. 服务发现配置不同: SpringCloud 使用 SPRING_CLOUD_ZOOKEEPER_CONNECT_STRING,Dubbo 使用 DUBBO_REGISTRY_ADDRESS
  2. 端口映射不同: SpringCloud Provider 使用 HTTP 端口 8080(映射到宿主机 8081),不需要 Dubbo 协议端口。
  3. 通信方式不同: Consumer 通过 OpenFeign(HTTP)调用 Provider,而非 Dubbo 协议。

第九章 生产环境最佳实践

9.1 镜像安全扫描

9.1.1 安全扫描工具

工具类型特点适用场景
Trivy开源快速、全面推荐(CI/CD 集成)
Snyk商业漏洞数据库全面企业级安全
Clair开源专注于 CVE 扫描Registry 集成
Docker ScoutDocker 官方与 Docker 深度集成Docker 生态

Trivy 扫描示例:

bash
# 安装 Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# 扫描镜像
trivy image smart-scaffold-springboot:latest

# 只扫描高危和严重漏洞
trivy image --severity HIGH,CRITICAL smart-scaffold-springboot:latest

9.1.2 在 CI/CD 中集成安全扫描

bash
#!/bin/bash
IMAGE_NAME="smart-scaffold-springboot:latest"

echo "=== 开始安全扫描 ==="
trivy image --severity HIGH,CRITICAL --exit-code 1 ${IMAGE_NAME}

if [ $? -ne 0 ]; then
    echo "错误: 发现高危或严重漏洞,构建终止"
    exit 1
fi

echo "=== 安全扫描通过 ==="

9.1.3 漏洞修复策略

漏洞等级修复时限策略
CRITICAL24 小时内立即修复,暂停部署
HIGH7 天内优先修复,评估风险
MEDIUM30 天内计划修复
LOW90 天内随版本更新修复

9.2 最小权限原则

9.2.1 容器用户权限

默认情况下,Docker 容器以 root 用户运行。推荐使用非 root 用户:

dockerfile
FROM azul/zulu-openjdk:17
WORKDIR /docker

# 创建应用用户和组
RUN groupadd -r appgroup && useradd -r -g appgroup -d /docker -s /sbin/nologin appuser

# 复制应用文件
COPY --from=builder /app/build/app.jar /docker/app.jar
COPY entrypoint.sh /docker/entrypoint.sh

# 设置文件所有权
RUN chown -R appuser:appgroup /docker && chmod +x /docker/entrypoint.sh

# 切换到非 root 用户
USER appuser

EXPOSE 8080
ENTRYPOINT ["/docker/entrypoint.sh"]

9.2.2 Docker 能力限制

bash
# 移除所有能力
docker run -d \
    --cap-drop ALL \
    --name smart-scaffold-springboot \
    smart-scaffold-springboot:latest

对于 Java 应用,通常不需要任何额外的 Linux Capabilities。

9.2.3 只读文件系统

bash
docker run -d \
    --read-only \
    --tmpfs /docker/logs:rw,noexec,nosuid,size=100m \
    --tmpfs /tmp:rw,noexec,nosuid,size=50m \
    --name smart-scaffold-springboot \
    smart-scaffold-springboot:latest

9.3 日志收集方案

9.3.1 Docker 日志驱动

日志驱动说明适用场景
json-file(默认)JSON 格式本地文件开发环境
journaldSystemd 日志Systemd 系统
fluentdFluentd 日志收集日志平台
gelfGraylog 格式Graylog

9.3.2 生产级日志配置

Docker 日志轮转:

bash
docker run -d \
    --name smart-scaffold-springboot \
    --log-driver json-file \
    --log-opt max-size=50m \
    --log-opt max-file=5 \
    --log-opt compress=true \
    smart-scaffold-springboot:latest

Spring Boot 日志配置(logback-spring.xml):

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

9.4 健康检查配置

9.4.1 Docker 健康检查

bash
docker run -d \
    --name smart-scaffold-springboot \
    --health-cmd="curl -f http://localhost:8080/actuator/health || exit 1" \
    --health-interval=30s \
    --health-timeout=5s \
    --health-retries=3 \
    --health-start-period=60s \
    -p 8080:8080 \
    smart-scaffold-springboot:latest

参数说明:

参数说明推荐值
--health-cmd健康检查命令curl -f http://localhost:8080/actuator/health
--health-interval检查间隔30s
--health-timeout超时时间5s
--health-retries连续失败次数3
--health-start-period启动宽限期60s

9.4.2 Dockerfile 中定义健康检查

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=60s \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

9.4.3 Spring Boot Actuator 配置

yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

9.5 资源限制(CPU/内存)

9.5.1 CPU 限制

bash
# 限制使用 2 个 CPU 核心
docker run -d \
    --cpus=2 \
    --name smart-scaffold-springboot \
    smart-scaffold-springboot:latest

# 或者使用 CPU 份额(相对权重)
docker run -d \
    --cpu-shares=1024 \
    --name smart-scaffold-springboot \
    smart-scaffold-springboot:latest

9.5.2 内存限制

bash
# 限制内存为 1GB
docker run -d \
    --memory=1g \
    --memory-swap=1g \
    --name smart-scaffold-springboot \
    smart-scaffold-springboot:latest

注意: --memory-swap=1g 禁止使用 swap,确保内存使用不会超过限制。

9.5.3 Docker Compose 资源限制

yaml
services:
  app:
    image: smart-scaffold-springboot:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
        reservations:
          cpus: '1.0'
          memory: 512M

9.5.4 资源限制与 JVM 参数的配合

资源限制必须与 JVM 参数配合使用。如果 Docker 限制了 1GB 内存,JVM 的 -Xmx 应设置为 768MB(75%):

bash
docker run -d \
    --memory=1g \
    --memory-swap=1g \
    --cpus=2 \
    -e JAVA_OPTS="-Xms512m -Xmx768m -XX:+UseG1GC -XX:+UseContainerSupport" \
    --name smart-scaffold-springboot \
    smart-scaffold-springboot:latest

9.6 镜像版本管理

9.6.1 版本命名规范

推荐使用语义化版本(Semantic Versioning):

<主版本>.<次版本>.<修订号>-<构建号>

示例:
1.0.0-42    (第 42 次构建)
1.0.1-88    (第 1 个修订版,第 88 次构建)
2.0.0-156   (第 2 个主版本,第 156 次构建)

9.6.2 镜像清理策略

bash
#!/bin/bash
# 清理旧镜像脚本

# 保留最近 10 个版本的镜像
docker images --format "{{.Repository}}:{{.Tag}}" | \
    grep "smart-scaffold" | \
    sort -t- -k2 -n | \
    head -n -10 | \
    xargs -r docker rmi

# 清理悬空镜像
docker image prune -f

# 清理未使用的资源
docker system prune -f

9.6.3 镜像签名与验证

使用 Docker Content Trust 确保镜像的完整性和来源可信:

bash
# 启用 Content Trust
export DOCKER_CONTENT_TRUST=1

# 推送签名镜像
docker push registry.example.com/smart-scaffold-springboot:1.0.0

# 拉取时自动验证签名
docker pull registry.example.com/smart-scaffold-springboot:1.0.0

第十章 总结与展望

10.1 全文回顾

本文基于 smart-scaffold 系列项目的实际经验,深入解析了 Java 微服务容器化部署的完整技术栈。让我们回顾一下核心要点:

容器化策略选择:

  • SpringBoot 单体应用采用"全量构建"模式,Dockerfile 内部完成全部构建过程。
  • Dubbo/SpringCloud 微服务采用"预构建"模式,CI/CD 统一构建后分别打包镜像。

多阶段构建核心价值:

  • 镜像体积缩减 60-75%,从 1.5GB 降至 400MB。
  • 攻击面最小化,运行时镜像不包含构建工具和源代码。
  • 构建环境一致性,消除"在我机器上能跑"的问题。

CI/CD 流水线设计:

  • Jenkins 作为核心自动化工具,覆盖代码检出、Maven 构建、Docker 镜像构建、SSH 远程部署四个阶段。
  • --restart=always 提供基本的自愈能力,结合健康检查实现更强大的容错机制。

生产环境安全加固:

  • 非 root 用户运行、Linux Capabilities 限制、只读文件系统。
  • Trivy 安全扫描、镜像签名验证、资源限制。

10.2 技术演进方向

短期(6 个月内):

  1. GraalVM Native Image: 将 Java 应用编译为原生可执行文件,启动时间从秒级降至毫秒级,内存占用减少 50-80%。
  2. Docker BuildKit 全面采用: 利用缓存挂载、并行构建等特性进一步优化构建速度。
  3. Jenkins Pipeline 即代码: 将所有 CI/CD 配置迁移到 Jenkinsfile,实现版本化管理。

中期(6-12 个月):

  1. Kubernetes 编排: 从 Docker Compose 迁移到 Kubernetes,实现自动扩缩容、滚动更新、服务网格等高级特性。
  2. Service Mesh: 引入 Istio 或 Linkerd,实现流量管理、熔断降级、分布式追踪。
  3. GitOps: 采用 ArgoCD 或 Flux,实现基于 Git 的声明式部署。

长期(12 个月以上):

  1. eBPF 网络可观测性: 利用 eBPF 技术实现零侵入的网络监控和性能分析。
  2. WebAssembly 容器: 探索 Wasm 作为容器的轻量级替代方案。
  3. AI 辅助运维: 利用机器学习进行容量规划、异常检测和自动修复。

10.3 smart-scaffold 项目展望

smart-scaffold 系列项目作为 Java 微服务脚手架,将持续演进以适应技术发展趋势:

  • 新增 Kubernetes 部署配置: 提供 Helm Chart 和 Kustomize 配置。
  • 集成 OpenTelemetry: 统一的分布式追踪和指标收集。
  • 支持多架构镜像: 同时支持 x86_64 和 ARM64 架构。
  • 提供 Terraform 基础设施即代码模板: 一键创建云上基础设施。

版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc