Java 应用的 Docker 化比 Go / Node 麻烦得多——JVM、字体、时区、HTTPS 证书、Tomcat 配置每一个都是坑。本篇把"从 Spring Boot fat jar 到可生产的 Java 容器"完整流程整理清楚。
阅读对象:Java 后端 / 运维工程师
覆盖范围:Tomcat 镜像选型 + Spring Boot fat jar 打包 + anapsix/alpine-java 镜像使用 + HTTPS 证书导入 + 时区 / 中文乱码 / 字体 / DNS 解决 + 镜像瘦身 + 完整 docker-compose 部署
一、Java 容器化三大流派
1.1 流派对比
| 方案 | 镜像 | 体积 | 启动速度 | 适用 |
|---|
| 完整 JDK | openjdk:8 | ~ 800 MB | 慢 | 需要 javac |
| JRE only | openjdk:8-jre-slim | ~ 250 MB | 中 | 生产应用 |
| Alpine JRE | anapsix/alpine-java:8u201b09_server-jre_nashorn | ~ 150 MB | 快 | 生产首选 |
| Native(GraalVM) | ghcr.io/graalvm/native-image | ~ 50 MB | 极快 | 性能要求高 |
默认推荐 anapsix/alpine-java——体积小、glibc 兼容好、社区维护。
1.2 为什么需要 alpine
- alpine 镜像小 5x(musl libc vs glibc)
- 冷启动快:JVM 加载速度快 30%+
- CVE 少:alpine 包更新频繁,CVE 修复更及时
二、Tomcat 镜像
2.1 官方 Tomcat 镜像
1
2
3
4
5
| docker pull tomcat:8.5
docker run -d -p 8098:8080 \
-v /data/my_tomcat_3/webapps:/usr/local/tomcat/webapps \
--name my_tomcat_3 \
tomcat:8.5
|
2.2 自建 Tomcat 镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # 1. 准备目录
mkdir tomcat_web && cd tomcat_web
touch Dockerfile start.sh
# 2. Dockerfile
cat > Dockerfile <<'EOF'
FROM tomcat:8.5
ADD ./webapps /usr/local/tomcat/webapps
CMD ["catalina.sh", "run"]
EOF
# 3. 构建
docker build -t tomcat_web:v2 .
# 4. 保存 / 加载(离线分发)
docker save -o tomcat_web.tar.gz tomcat_web:v2
docker load -i tomcat_web.tar.gz
|
2.3 完整 Tomcat 部署示例
1
2
3
4
5
6
7
8
9
10
11
12
| # 创建工作目录
mkdir -p /home/epadmZZ && cd /home/epadmZZ
touch server.xml
mkdir logs ROOT
# 部署容器
docker run -tid --restart=always --name epadmZZ \
-p 28081:8080 \
-v /home/project/epadmZZ/:/Webs \
-v /etc/localtime:/etc/localtime \
-m 2048m --memory-swap=2048m --cpu-shares=256 \
tomcat_web:v2 start.sh run -config /Webs/server.xml
|
关键参数:
-m 2048m --memory-swap=2048m:内存硬限——避免 OOM 影响宿主机--cpu-shares 256:CPU 权重(默认 1024,相对值)-v /etc/localtime:/etc/localtime:时区同步
三、Spring Boot fat jar 容器化
3.1 构建 Spring Boot 应用
1
2
| mvn clean package
# 生成 target/myapp-1.0.0.jar
|
3.2 最小化 Dockerfile
1
2
3
4
5
| FROM anapsix/alpine-java:8u201b09_server-jre_nashorn
VOLUME /tmp
ADD myapp-1.0.0.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
3.3 启动参数注入
1
2
3
4
5
6
7
8
| # 普通启动
docker run -d --restart=always \
--name myapp \
-p 8080:8080 \
-v /home/project/myapp:/jar \
-v /etc/localtime:/etc/localtime \
anapsix/alpine-java:8u201b09_server-jre_nashorn \
java -jar -Duser.timezone=GMT+08 /jar/myapp-1.0.0.jar
|
多端口 + 特殊权限(IoT 项目):
1
2
3
4
5
6
7
8
9
| docker run -d --restart=always \
-v /home/project/iot:/jar \
-v /home/project/iot/logs:/kde-iot-log \
-v /etc/localtime:/etc/localtime \
-p 8080:8080 -p 443:443 -p 18888:18888 \
--net mynetwork --ip 172.18.0.5 \
--privileged=true \
--name iot anapsix/alpine-java:8u201b09_server-jre_nashorn \
java -jar -Duser.timezone=GMT+08 /jar/iot-0.0.1-SNAPSHOT.jar
|
3.4 JVM 参数调优
1
2
3
4
5
6
7
| docker run -d \
-e JAVA_OPTS="-Xms2g -Xmx2g -Xmn1g -Xss1m \
-XX:SurvivorRatio=8 \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=256m \
-XX:NativeMemoryTracking=detail" \
myapp:latest
|
容器内 JVM 调优要点:
- JVM 看到的 CPU 数可能与宿主机不同——加
-XX:+UseContainerSupport(Java 10+) - JVM 看到的内存可能不准——加
-XX:MaxRAMPercentage=75.0 - 容器感知:
java -XX:+PrintFlagsFinal -version | grep UseContainerSupport
1
| ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+PrintFlagsFinal"
|
四、HTTPS 证书导入
4.1 场景
小程序后台 + 数据接收 + 第三方 HTTPS 对接——需要 SSL 证书。
4.2 阿里云免费证书申请
1
2
3
4
| 1. 阿里云控制台 → SSL 证书 → 购买证书
2. 选择:单域名 / DV SSL / 免费版 / DigiCert
3. 域名验证(DNS 解析添加 TXT 记录)
4. 下载证书:Tomcat 类型(.pfx + password.txt)
|
4.3 PFX → JKS 转换
容器内 Java 用 JKS 格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 1. 进入 JDK 容器
docker exec -it myapp bash
# 2. 找 keytool
which keytool
# /usr/lib/jvm/default-jvm/bin/keytool
# 3. 转换 PFX → JKS
keytool -importkeystore \
-srckeystore /path/to/xxx.pfx \
-destkeystore /app/wx.jks \
-srcstoretype PKCS12 \
-deststoretype JKS
# 4. 输入 PFX 密码(password.txt 中的密码)
# 5. 记好 alias(jks 文件别名)
|
4.4 挂载到容器
1
2
3
| docker run -d \
-v /home/project/wx.jks:/app/wx.jks \
myapp:latest
|
4.5 Spring Boot 配置
1
2
3
4
5
6
7
| server:
port: 443
ssl:
key-store: classpath:wx.jks
key-store-password: {{JKS_PASS}}
key-store-type: JKS
key-alias: alias
|
4.6 强制 HTTPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| @SpringBootApplication
public class Application {
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
tomcat.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}
@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(443);
return connector;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
|
效果:
- 80 / 8080 收到请求 → 自动 302 重定向到 443
- 443 走 HTTPS
五、时区问题
5.1 三种方法
方法 A:挂载宿主机时区(最简单)
1
| docker run -v /etc/localtime:/etc/localtime:ro ...
|
方法 B:环境变量 TZ(Java 8u72+)
1
| docker run -e TZ=Asia/Shanghai ...
|
方法 C:JVM 参数
1
| java -Duser.timezone=GMT+08 -jar app.jar
|
生产推荐 A + B 一起用。
5.2 容器内修改
1
2
3
4
5
6
| # 进入容器
docker exec -it 2c87bcc41378 /bin/bash
# 拷贝时区文件
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
echo "Asia/Shanghai" > /etc/timezone
|
alpine 镜像特殊处理:
1
2
3
| RUN apk add --no-cache tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
|
六、中文乱码
6.1 三处必设
Dockerfile / 启动参数:
1
2
3
4
5
| java -Dfile.encoding=UTF-8 \
-Dsun.jnu.encoding=UTF-8 \
-Duser.language=zh \
-Duser.country=CN \
-jar app.jar
|
MySQL 连接串:
1
2
3
| spring:
datasource:
url: jdbc:mysql://10.0.1.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8
|
Linux locale(容器内):
1
2
3
4
5
6
7
| docker exec -it myapp locale
# 看到 POSIX / C 是错的
# 设置
docker exec -it myapp bash
localedef -i en_US -f UTF-8 en_US.UTF-8
export LANG=en_US.UTF-8
|
根治:
1
2
| ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
|
七、字体问题
7.1 症状
Java 应用:
- 导出 PDF 中文变方块
- 生成图形验证码 OOM
- POI 操作 Excel 报错
原因:JDK 本身不带字体,alpine / slim 镜像里 fontconfig 也没装。
7.2 修复
Debian / Ubuntu 基础:
1
| RUN apt-get update && apt-get install -y fontconfig
|
Alpine 基础(alpine 3.15+ 必需):
1
| RUN apk add --no-cache fontconfig
|
完整中文字体(把 Windows 字体拷进容器):
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 1. 宿主机准备字体目录
mkdir -p /home/fonts
# 2. 从 Windows 拷贝
# C:\Windows\Fonts\msyh.ttc (微软雅黑)
# C:\Windows\Fonts\simfang.ttf (仿宋)
# C:\Windows\Fonts\simhei.ttf (黑体)
# C:\Windows\Fonts\simsun.ttc (宋体)
# 3. 挂载到容器
docker run \
-v /home/fonts:/usr/share/fonts/win \
...
|
容器内更新字体缓存:
1
2
3
| docker exec -it myapp bash
fc-cache -fv
fc-list :lang=zh
|
7.3 Spring Boot PDF 字体
1
2
3
4
5
6
7
8
9
| @Bean
public PdfReportFontConfig pdfFontConfig() {
return new PdfReportFontConfig() {
@Override
public String getFontFile() {
return "/usr/share/fonts/win/msyh.ttc";
}
};
}
|
八、容器化常用工具
8.1 必备工具清单
1
2
3
4
5
| # Alpine 镜像(最小化,几乎啥都没有)
apk add --no-cache curl wget iputils-ping iproute2 bash vim fontconfig tzdata
# Debian 镜像
apt-get install -y curl wget iputils-ping iproute2 vim fontconfig dnsutils
|
8.2 容器内调试
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 进容器
docker exec -it myapp bash
# 看进程
ps -ef
ps aux
# 看端口
netstat -tnlp # alpine: apk add net-tools
ss -tnlp # 通用
# 看内存
free -h
|
九、DNS 问题
症状:容器内 ping internal.example.com 失败。
修复:
1
2
3
4
5
6
7
8
9
| # 1. 修改 docker daemon DNS
vim /etc/docker/daemon.json
{
"dns": ["114.114.114.114", "8.8.8.8"]
}
systemctl restart docker
# 2. 或容器内指定
docker run --dns 114.114.114.114 --dns 8.8.8.8 ...
|
十、镜像瘦身
10.1 多阶段构建
1
2
3
4
5
6
7
8
9
10
11
12
| # 阶段 1:编译
FROM maven:3.9-eclipse-temurin-17 AS build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# 阶段 2:运行时
FROM anapsix/alpine-java:8u201b09_server-jre_nashorn
COPY --from=build /target/myapp-1.0.0.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
效果:编译镜像 ~ 800MB,运行时镜像 ~ 150MB。
10.2 清理缓存
1
2
| RUN apt-get update && apt-get install -y fontconfig \
&& rm -rf /var/lib/apt/lists/* # 必须清!
|
10.3 JLink 定制 JRE
1
2
3
4
5
6
7
8
9
10
11
12
| # 用 jlink 裁剪 JDK(Java 9+)
jlink \
--module-path $JAVA_HOME/jmods \
--add-modules java.base,java.logging,java.sql,java.naming,java.management,java.security.jgss,java.desktop \
--output /opt/jre-mini \
--strip-debug --no-man-pages --compress=2
# 用自定义 JRE 做基础镜像
FROM scratch
COPY --from=builder /opt/jre-mini /opt/jre
ENV JAVA_HOME=/opt/jre
ENV PATH=$JAVA_HOME/bin:$PATH
|
效果:JRE 50MB 级别。
十一、完整生产 Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| # 阶段 1:构建
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 阶段 2:运行时
FROM anapsix/alpine-java:8u201b09_server-jre_nashorn
# 必备工具
RUN apk add --no-cache \
fontconfig \
tzdata \
tini \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& fc-cache -fv
# JVM 容器感知
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
# 中文环境
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
TZ=Asia/Shanghai
WORKDIR /app
COPY --from=build /build/target/*.jar /app/app.jar
# 暴露端口
EXPOSE 8080
# tinit 收僵尸进程
ENTRYPOINT ["/sbin/tini", "--", "sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
|
十二、典型坑位
12.1 容器里 ping 找不到
1
| RUN apt-get install -y iputils-ping
|
12.2 容器里 curl / wget 找不到
1
| RUN apt-get install -y curl wget
|
12.3 catalina.out 无限膨胀
1
2
3
4
| # 清空
cat /dev/null > /opt/sxt/logs/catalina.out
# 配置 logrotate
|
12.4 DNS 解析失败
1
2
| # 容器内
docker run --dns 114.114.114.114 ...
|
12.5 内存超限被 OOM
1
2
3
4
5
| # 加硬限
docker run -m 2g --memory-swap=2g ...
# JVM 也限
java -Xmx1500m ...
|
十三、生产部署清单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # 1. 构建
docker build -t myapp:20240315 .
# 2. 保存离线
docker save -o myapp.tar.gz myapp:20240315
# 3. 推 Harbor
docker push myapp.example.com/library/myapp:20240315
# 4. 部署
docker run -d --restart=always \
--name myapp \
--net=mynetwork \
-p 8080:8080 \
-v /home/myapp/logs:/app/logs \
-v /home/myapp/config:/app/config \
-v /etc/localtime:/etc/localtime:ro \
-m 2g --memory-swap=2g \
myapp.example.com/library/myapp:20240315
# 5. 接入日志
docker logs -f myapp 2>&1 | tee -a /var/log/myapp/app.log
|
十四、最佳实践清单
- JRE 镜像而非 JDK(生产不需要 javac)
anapsix/alpine-java 默认推荐(社区维护 + glibc 兼容)-XX:+UseContainerSupport JVM 容器感知-XX:MaxRAMPercentage=75.0 内存自适应-XX:+ExitOnOutOfMemoryError OOM 退出(让编排系统重启)tini 收僵尸进程(Java 不会自动 reap)LANG=C.UTF-8 + 字体 解决中文乱码/etc/localtime 挂载 解决时区-m 2g --memory-swap=2g 硬限内存- 多阶段构建 镜像瘦身
2024+ 视角补充
本文写于 2018-12,2024-2026 期间 Java 应用容器化关键演进:
- JDK 17 / 21 主流化:JDK 8 官方支持 2024-03 已结束(最后 LTS)——新项目必须 JDK 17 或 21;JDK 21(2023-09 LTS,支持到 2031)已成 2024+ 主流
- Spring Boot 3.2+ 全面 Jakarta EE:从
javax.* 迁移到 jakarta.*——Tomcat 10+ 是默认;JDK 17 最低要求 - Spring Boot 3.4+(2025):Virtual Threads(虚拟线程)默认集成——JDK 21 协程天然适配
- GraalVM Native Image 1.0+(2024-2026):Spring Boot 3.x 官方支持——
spring-boot:build-image 直接生成 native 可执行文件,启动 < 100ms,内存 < 100MB - CRaC(Coordinated Restore at Checkpoint):OpenJDK 17+ 实验,JVM 启动 < 10ms——Serverless / K8s 冷启动杀手
- 容器镜像生态:
- Eclipse Temurin(2024-2026):前 Adoptium,JDK 21 主流发行版——OpenJDK + 商业支持
- BellSoft Liberica JDK(2024-2026):Native Image Kit + Alpine glibc 兼容——容器首选
- Azul Zulu Prime(2024-2026):C4 Pauseless GC——延迟敏感场景
- Amazon Corretto(2024-2026):AWS 维护,生产级 LTS
- 多阶段构建 已成 Java 容器化标准
- JLink 定制 JRE:50MB 级别运行时镜像
实战建议(2025-2026 视角):
- 新项目 → JDK 21 LTS + Spring Boot 3.4+ + GraalVM Native Image(Serverless / 冷启动敏感场景)
- 存量项目 → 升 JDK 17 LTS(避免 JDK 8 / 11 停服)→ 再逐步升 21
- Serverless / K8s 冷启动 → GraalVM Native Image 或 CRaC
- 基础镜像:BellSoft Liberica JDK 21 是 2024+ 容器首选
下一步