Appearance
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。
体积膨胀带来的直接影响包括:
- 镜像拉取时间延长: 在 CI/CD 流水线中,每次部署都需要拉取镜像。一个 500MB 的镜像在良好的网络环境下需要 30 秒到 1 分钟,而在网络条件较差的环境下可能需要数分钟。
- 存储空间消耗: Docker 镜像采用分层存储,每一层都会占用磁盘空间。在频繁部署的场景下,大量历史镜像层会快速消耗服务器存储。
- 安全扫描时间增加: 镜像体积越大,安全扫描所需的时间越长,这在自动化流水线中会成为瓶颈。
- 容器启动速度降低: 虽然容器启动速度主要取决于应用本身,但较大的镜像层解压也需要额外时间。
1.1.2 JVM 参数调优
Java 虚拟机(JVM)的参数调优是容器化部署中另一个核心挑战。传统虚拟机或物理机部署时,运维人员通常根据硬件配置手动设置 JVM 参数。但在容器化环境中,情况发生了根本性变化。
容器环境下的 JVM 参数挑战:
内存感知问题: 早期版本的 JVM 无法感知容器的内存限制(cgroups),
-Xmx参数设置不当可能导致容器被 OOM Killer 杀掉。从 Java 8u191 和 Java 11 开始,JVM 才较好地支持了容器内存感知,但仍需注意一些边界情况。CPU 感知问题: 类似地,JVM 的 GC 线程数默认基于物理 CPU 核心数计算,而非容器分配的 CPU 配额。这可能导致 GC 线程数过多,反而降低性能。
堆内存与直接内存的平衡: 在容器化环境中,堆内存(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/ (第三方依赖)容器化环境中的类加载问题:
- 类冲突: 当应用依赖的库版本与基础镜像中自带的库版本不一致时,可能产生类冲突。使用多阶段构建可以有效避免这一问题,因为运行阶段的基础镜像只包含 JRE,不包含额外的库。
- 热部署限制: 在开发环境中,Spring Boot DevTools 提供了热重载功能。但在容器化部署中,由于文件系统的隔离性,热部署机制需要特殊配置才能工作。
- JNI 库加载: 如果应用依赖本地库(JNI),在容器化环境中需要确保这些库在运行阶段的基础镜像中可用。
- 类路径隔离: 在微服务架构中,不同服务可能依赖同一库的不同版本。容器化天然提供了进程级隔离,每个服务运行在独立的容器中,从根本上避免了类路径冲突。
1.1.4 容器化与 Java 版本选择
在 smart-scaffold 系列项目中,我们统一选择了 Java 17 作为目标版本。这个选择并非随意,而是基于以下考量:
| 特性 | Java 8 | Java 11 | Java 17 | Java 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 单阶段) |
+---------------------------------------------------+多阶段构建的关键优势:
- 镜像体积缩减 60-75%: 从 1.5GB 降至 400MB,大幅节省存储和传输成本。
- 攻击面最小化: 运行时镜像不包含源代码、构建工具和开发依赖,显著降低了安全风险。
- 构建一致性: 构建环境完全封装在 Docker 镜像中,消除了"在我机器上能跑"的问题。
- 缓存友好: Docker 的层缓存机制可以复用未变化的构建层,加速后续构建。
- 灵活性: 不同的阶段可以使用完全不同的基础镜像,构建阶段用 Maven 镜像,运行阶段用精简的 JRE 镜像。
1.2.3 smart-scaffold 项目的镜像体积对比
基于实际测试数据,以下是 smart-scaffold 三个版本在不同构建策略下的镜像体积对比:
| 构建策略 | SpringBoot 版 | Dubbo Provider | SpringCloud 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.xmlDubbo 版采用 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.xmlSpringCloud 版采用 REST 微服务架构,通过 OpenFeign 进行服务间调用,通过 Zookeeper 进行服务发现。
1.3.2 容器化方案对比矩阵
| 维度 | SpringBoot 版 | Dubbo 版 | SpringCloud 版 |
|---|---|---|---|
| 镜像数量 | 1 个 | 2 个 | 2 个 |
| Dockerfile 数量 | 1 个 | 2 个 | 2 个 |
| 构建复杂度 | 低 | 中 | 中 |
| 网络依赖 | 无 | Zookeeper | Zookeeper |
| 部署顺序 | 单步 | Provider -> Consumer | Provider -> Consumer |
| 服务发现 | N/A | Zookeeper | Zookeeper |
| CI/CD 复杂度 | 低 | 中 | 中 |
| 适合场景 | 中小型项目、快速迭代 | 高性能 RPC 场景 | 云原生微服务场景 |
1.3.3 为什么选择不同的容器化策略
这三个项目虽然属于同一系列,但它们的架构差异决定了不同的容器化策略:
SpringBoot 版采用"全量构建"模式——Dockerfile 内部完成从源代码到可执行 JAR 的全部构建过程。这是因为单体应用只有一个可部署单元,构建流程简单直接。
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_Store3. 构建缓存(Build Cache)
Docker 的构建缓存是加速构建的关键。缓存命中规则如下:
- 对于
ADD和COPY指令,Docker 会计算文件的校验和。如果文件内容没有变化,则命中缓存。 - 对于其他指令(
RUN、ENV等),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结束,生成最终镜像 ----关键要点:
- 阶段命名: 使用
AS builder和AS runner为阶段命名,使COPY --from=builder更加清晰可读。 - 选择性复制: 只有通过
COPY --from显式复制的文件才会出现在最终镜像中。 - 独立缓存: 每个阶段有独立的构建缓存。修改阶段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 Temurin | Eclipse 基金会 | GPL v2 + CE | 良好 | 推荐 |
| Azul Zulu | Azul Systems | GPL v2 + CE | 优秀 | 推荐 |
| Amazon Corretto | AWS | GPL v2 + CE | 良好 | 推荐 |
| Oracle JDK | Oracle | OTN(商业) | 一般 | 不推荐 |
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这段代码做了以下几件事:
- 创建
.m2目录:mkdir -p ~/.m2确保 Maven 配置目录存在。 - 设置本地仓库路径:
<localRepository>/tmp/m2repo</localRepository>将 Maven 本地仓库设置到/tmp/m2repo。选择/tmp目录的原因是它在容器重启后会被自动清理,避免仓库缓存无限增长。 - 禁用离线模式:
<offline>false</offline>确保 Maven 在需要时可以从远程仓库下载依赖。 - 清空镜像配置:
<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 || truemvnw(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 的优势:
- 经过认证的 OpenJDK 构建: Azul Systems 是 Java 生态系统中久负盛名的公司,其 Zulu 版 OpenJDK 通过了 TCK(Technology Compatibility Kit)认证。
- 优秀的容器化支持: Azul 提供了专门优化的容器镜像,包括精简版(Alpine-based)和标准版(Debian-based)。
- 及时的安全更新: Azul 以快速发布安全补丁著称,通常在 Oracle 发布 CVE 修复后 24 小时内提供对应版本。
- 长期支持承诺: Azul 为每个 LTS 版本提供至少 10 年的支持周期。
镜像体积对比:
| 镜像 | 体积 | 包含内容 | 适用场景 |
|---|---|---|---|
azul/zulu-openjdk:17 | ~300MB | JRE + 常用工具 | 生产环境(推荐) |
azul/zulu-openjdk-alpine:17 | ~180MB | JRE(精简) | 对体积敏感的场景 |
eclipse-temurin:17-jre | ~220MB | JRE | 通用场景 |
eclipse-temurin:17-jre-alpine | ~150MB | JRE(精简) | 极致体积优化 |
选择 azul/zulu-openjdk:17 而非 Alpine 版本的原因是:标准版基于 Debian,提供了更好的 glibc 兼容性。一些 Java 库(特别是涉及 JNI 的库)在 Alpine 的 musl libc 上可能存在兼容性问题。对于生产环境,稳定性优先于体积优化。
2.3.2 工作目录设置
dockerfile
WORKDIR /docker将工作目录设置为 /docker 而非默认的 / 或 /app,有以下好处:
- 语义清晰:
/docker目录名明确表示这是 Docker 容器内的应用目录。 - 避免冲突: 避免与系统目录(如
/app、/opt)产生冲突。 - 路径一致性: 与
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/这段代码的执行流程:
- 列出构建产物:
ls -l /app/smart-scaffold-web/target/打印构建产物列表,便于调试。 - 创建输出目录:
mkdir -p /app/build确保输出目录存在。 - 提取 JAR 文件:
first_jar=$(ls /app/smart-scaffold-web/target/*.jar | head -n 1)获取第一个匹配的 JAR 文件。
注意: 这里使用 head -n 1 而不是 grep -v original。spring-boot-maven-plugin 的 repackage 目标会生成两个文件:
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 | 不确定 | 不推荐 |
| 排除 original | grep -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-plugin 的 repackage 目标是关键——它将普通的 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。后续的 COPY、RUN、CMD 等指令都以此目录为基准。
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 1Cdockerfile
# 构建:生成 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% |
| 仅修改 POM | 5 分钟 | 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(短连接) |
| 服务发现 | Zookeeper | Zookeeper |
| 负载均衡 | Dubbo 内置 | Spring Cloud LoadBalancer |
| 端口暴露 | Provider: 20880, Consumer: 8080 | Provider: 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 的关键差异:
- 没有 Maven 构建步骤: Provider Dockerfile 中没有
mvn clean package命令。它假设 JAR 文件已经通过外部构建生成。 - 从根目录 COPY 全部源码:
COPY . .将整个项目(包括所有子模块)复制到构建阶段。 - 精确的 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 远程部署 |
+----------------------------------------------------------+预构建模式的优势:
- 构建一次,多处使用: Maven 只需执行一次,所有模块的 JAR 都会被构建。
- 并行镜像构建: Provider 和 Consumer 的 Docker 镜像可以并行构建。
- 构建缓存效率高: 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虽然只有两行代码,但这个脚本承载了几个重要的职责:
- 标准化启动入口: 无论底层使用什么框架,启动方式统一为
java -jar。 - Shell 脚本灵活性: 使用 Shell 脚本而非直接在 Dockerfile 中写
CMD,可以方便地添加环境检查、参数注入等逻辑。 - 信号处理: 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=prod5.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) | 元空间 | 直接内存 | 线程栈 | 缓冲 |
|---|---|---|---|---|---|
| 512MB | 384MB | 64MB | 32MB | ~32MB | ~0MB |
| 1GB | 768MB | 128MB | 64MB | ~32MB | ~8MB |
| 2GB | 1536MB | 256MB | 128MB | ~64MB | ~16MB |
| 4GB | 3072MB | 256MB | 256MB | ~128MB | ~288MB |
原则2:使用 G1GC 作为默认垃圾收集器
从 Java 9 开始,G1GC 已成为默认的垃圾收集器。对于容器化环境,G1GC 的优势尤为明显:可预测的停顿时间、适合容器化、内存碎片少。
原则3:启用容器感知
bash
java -XX:+UseContainerSupport -jar /docker/app.jarJava 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:+HeapDumpOnOutOfMemoryError | OOM 时自动导出堆转储 | 开启 |
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=prod5.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关键设计要点:
set -e: 任何命令失败时立即退出。exec java: 使用exec替换当前 Shell 进程,使 Java 进程成为容器的 PID 1。- 环境预检查: 启动前检查 JAR 文件是否存在且完整。
- 参数分层: 将 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) 运行容器 |
+-------------------------------------------------------------+流水线阶段详解:
- 代码检出(Checkout): 从 Git 仓库拉取最新代码。
- Maven 构建(Build): 编译源代码,运行单元测试,生成 JAR 文件。
- Docker 镜像构建(Package): 基于 Dockerfile 构建应用镜像。
- 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.xmlBuild 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:latest6.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 | 代码版本追溯 | 永久保留 |
| latest | latest | 默认拉取 | 自动更新 |
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_SCRIPT6.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-springbootdocker 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:latest6.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-netJenkins 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 | 包含内容 | 适用场景 |
|---|---|---|---|
| central | https://maven.aliyun.com/repository/central | Maven Central | 通用依赖 |
| public | https://maven.aliyun.com/repository/public | Central + JCenter + Google | 推荐(聚合仓库) |
| apache-snapshots | https://maven.aliyun.com/repository/apache-snapshots | Apache 快照版 | 快照依赖 |
| spring | https://maven.aliyun.com/repository/spring | Spring 系列依赖 | 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: bridge8.3 数据持久化
| 方式 | 语法 | 数据位置 | 适用场景 |
|---|---|---|---|
| Volume | db-data:/var/lib/mysql | Docker 管理 | 数据库数据(推荐) |
| 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 版的主要差异:
- 服务发现配置不同: SpringCloud 使用
SPRING_CLOUD_ZOOKEEPER_CONNECT_STRING,Dubbo 使用DUBBO_REGISTRY_ADDRESS。 - 端口映射不同: SpringCloud Provider 使用 HTTP 端口 8080(映射到宿主机 8081),不需要 Dubbo 协议端口。
- 通信方式不同: Consumer 通过 OpenFeign(HTTP)调用 Provider,而非 Dubbo 协议。
第九章 生产环境最佳实践
9.1 镜像安全扫描
9.1.1 安全扫描工具
| 工具 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Trivy | 开源 | 快速、全面 | 推荐(CI/CD 集成) |
| Snyk | 商业 | 漏洞数据库全面 | 企业级安全 |
| Clair | 开源 | 专注于 CVE 扫描 | Registry 集成 |
| Docker Scout | Docker 官方 | 与 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:latest9.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 漏洞修复策略
| 漏洞等级 | 修复时限 | 策略 |
|---|---|---|
| CRITICAL | 24 小时内 | 立即修复,暂停部署 |
| HIGH | 7 天内 | 优先修复,评估风险 |
| MEDIUM | 30 天内 | 计划修复 |
| LOW | 90 天内 | 随版本更新修复 |
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:latest9.3 日志收集方案
9.3.1 Docker 日志驱动
| 日志驱动 | 说明 | 适用场景 |
|---|---|---|
json-file(默认) | JSON 格式本地文件 | 开发环境 |
journald | Systemd 日志 | Systemd 系统 |
fluentd | Fluentd 日志收集 | 日志平台 |
gelf | Graylog 格式 | 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:latestSpring 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 19.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: true9.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:latest9.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: 512M9.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:latest9.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 -f9.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 个月内):
- GraalVM Native Image: 将 Java 应用编译为原生可执行文件,启动时间从秒级降至毫秒级,内存占用减少 50-80%。
- Docker BuildKit 全面采用: 利用缓存挂载、并行构建等特性进一步优化构建速度。
- Jenkins Pipeline 即代码: 将所有 CI/CD 配置迁移到 Jenkinsfile,实现版本化管理。
中期(6-12 个月):
- Kubernetes 编排: 从 Docker Compose 迁移到 Kubernetes,实现自动扩缩容、滚动更新、服务网格等高级特性。
- Service Mesh: 引入 Istio 或 Linkerd,实现流量管理、熔断降级、分布式追踪。
- GitOps: 采用 ArgoCD 或 Flux,实现基于 Git 的声明式部署。
长期(12 个月以上):
- eBPF 网络可观测性: 利用 eBPF 技术实现零侵入的网络监控和性能分析。
- WebAssembly 容器: 探索 Wasm 作为容器的轻量级替代方案。
- AI 辅助运维: 利用机器学习进行容量规划、异常检测和自动修复。
10.3 smart-scaffold 项目展望
smart-scaffold 系列项目作为 Java 微服务脚手架,将持续演进以适应技术发展趋势:
- 新增 Kubernetes 部署配置: 提供 Helm Chart 和 Kustomize 配置。
- 集成 OpenTelemetry: 统一的分布式追踪和指标收集。
- 支持多架构镜像: 同时支持 x86_64 和 ARM64 架构。
- 提供 Terraform 基础设施即代码模板: 一键创建云上基础设施。
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc。