Featured image of post Java 应用 Docker 化:Tomcat / Spring Boot 打包 + HTTPS / 时区 / 字体坑

Java 应用 Docker 化:Tomcat / Spring Boot 打包 + HTTPS / 时区 / 字体坑

Java Web 应用 Docker 化全流程:Tomcat 镜像选型、Spring Boot fat jar 打包、HTTPS 证书导入、时区与中文乱码解决、镜像瘦身

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 流派对比

方案镜像体积启动速度适用
完整 JDKopenjdk:8~ 800 MB需要 javac
JRE onlyopenjdk:8-jre-slim~ 250 MB生产应用
Alpine JREanapsix/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 256CPU 权重(默认 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/*   # 必须清!
 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 定制 JRE50MB 级别运行时镜像

实战建议(2025-2026 视角)

  • 新项目JDK 21 LTS + Spring Boot 3.4+ + GraalVM Native Image(Serverless / 冷启动敏感场景)
  • 存量项目 → 升 JDK 17 LTS(避免 JDK 8 / 11 停服)→ 再逐步升 21
  • Serverless / K8s 冷启动GraalVM Native ImageCRaC
  • 基础镜像BellSoft Liberica JDK 21 是 2024+ 容器首选

下一步

使用 Hugo 构建
主题 StackJimmy 设计