一、Java 项目容器化的核心思路

Java 服务镜像制作通常可以拆成两部分:

  • 先在构建环境里执行 Maven 打包
  • 再把生成的 jar 放进运行镜像

这也是最典型的“构建环境”和“运行环境”分离思路。

二、准备源码、缓存目录和构建镜像

原始示例使用的源码地址:

https://gitee.com/dukuan/spring-boot-project.git

构建镜像:

registry.cn-hangzhou.aliyuncs.com/abroad_images/maven:3.5.3

构建命令:

mvn clean install -DskipTests

为了让 Maven 依赖缓存能够复用,先在宿主机准备缓存目录:

mkdir -p /root/java/m2

三、为什么推荐用临时容器做 Maven 构建

示例里使用临时容器完成构建:

docker run --ulimit nofile=1024 -it --rm \
  -v `pwd`/m2:/root/.m2 \
  -v `pwd`/spring-boot-project:/mnt/ \
  registry.cn-hangzhou.aliyuncs.com/abroad_images/maven:3.5.3 bash

进入容器后执行:

cd /mnt
mvn clean install -DskipTests

这种方式的好处是:

  • 宿主机不用直接安装 Maven 和 JDK
  • 团队构建环境统一
  • Maven 缓存可通过挂载目录重复利用

只要输出中出现 BUILD SUCCESS,就说明打包已经成功。

四、如何编写 Java 运行镜像的 Dockerfile

打包成功后,宿主机目录下会出现 target 目录和对应的 jar 包。运行镜像可以写得非常简洁:

FROM registry.cn-hangzhou.aliyuncs.com/abroad_images/jre:8u211-data
COPY target/spring-cloud-eureka-0.0.1-SNAPSHOT.jar ./app.jar
ENV JAVA_OPTS="-XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=70.0"
CMD java -jar app.jar

这里有几个关键点:

  • 基础镜像只保留运行 JRE,不再保留构建工具
  • 把实际业务 jar 复制为统一名称 app.jar
  • 用环境变量控制 JVM 堆内存占比

五、构建和启动 Java 镜像

构建镜像:

docker build -t registry.cn-hangzhou.aliyuncs.com/abroad_images/java:v1 .

启动容器前,还要先确认应用本身监听的端口。原始示例中 application.yml 显示端口为 8761

因此启动命令可以写成:

docker run -d -p 18761:8761 -m 1g --ulimit nofile=65535:65535 \
  registry.cn-hangzhou.aliyuncs.com/abroad_images/java:v1

运行后可以用 curl 10.0.0.12:18761 访问验证。

Java 服务访问验证

六、为什么 Java 容器经常要额外关注资源参数

Java 在容器环境里比很多轻量语言更依赖资源边界设置,因为:

  • JVM 会根据可见资源进行内存和线程相关决策
  • 文件描述符限制不合理时,构建和运行都可能出问题
  • 某些旧版本 JRE/JDK 在容器里对资源探测并不总是理想

因此 Java 镜像不仅要会做,还要会调运行参数。

七、常见问题一:Maven 构建时报文件描述符或内存异常

原始示例在执行 mvn clean install -DskipTests 时,遇到了类似下面的错误:

library initialization failed - unable to allocate file descriptor table - out of memory

解决思路是给构建容器增加合适的 ulimit 控制,例如:

docker run --ulimit nofile=1024 -it --rm ...

这说明问题不一定是 Maven 本身,而可能是容器可用文件描述符数量太小。

八、常见问题二:容器启动后 Exited(127) 或 JVM 崩溃

原始笔记还记录了一类比较典型的运行异常:

  • 容器状态为 Exited (127)
  • 日志中出现 JVM fatal error、SIGSEGV
  • 同时伴随 unable to allocate file descriptor table - out of memory

这种情况通常需要同时从两个方向处理:

8.1 限制容器内存边界

-m 1g

8.2 调整文件描述符限制

--ulimit nofile=65535:65535

原始验证结果表明,这两个参数单独增加其中一个,在实际环境中都可能帮助解决问题。

九、Java 镜像实战里最值得记住的经验

结合 Day004 的 Java 部分,可以沉淀出几条很有价值的经验:

  • Maven 构建和运行镜像尽量分离
  • 构建缓存目录最好挂载到宿主机,避免重复下载依赖
  • 启动前先确认应用真实监听端口
  • Java 镜像要重点关注内存和 ulimit
  • 遇到 Exited (127) 不要只盯着 Docker 命令,要先看日志

Java 容器化最难的通常不是写 Dockerfile,而是处理“资源限制、启动参数和运行故障”这几个细节。把这些细节打磨好,Java 服务的镜像交付会稳定很多。