ElIauk

用 Docker 运行 Spring Boot 应用入门

为什么用 Docker Spring Boot 打成 jar 之后,理论上 java -jar 就能跑,但“本地能跑、服务器跑不起来”的情况太常见了:JDK 版本不一致、时区不对、缺字体、环境变量没配。Docker 把应用和运行环境一起打包成镜像,换台机器只要装 Docker 就能跑出同样的结果。

评论
用 Docker 运行 Spring Boot 应用入门

为什么用 Docker

Spring Boot 打成 jar 之后,理论上 java -jar 就能跑,但“本地能跑、服务器跑不起来”的情况太常见了:JDK 版本不一致、时区不对、缺字体、环境变量没配。Docker 把应用和运行环境一起打包成镜像,换台机器只要装 Docker 就能跑出同样的结果。

2022 年用 Docker 跑 Spring Boot 已经是常规操作,入门只需要掌握三件事:写 Dockerfile、构建镜像、运行容器。

先打包

mvn clean package -DskipTests
ls -lh target/*.jar

-DskipTests 只跳过测试执行,测试代码仍会编译;想连测试编译一起跳过用 -Dmaven.test.skip=true。spring-boot-maven-plugin 打出来的是 fat jar,里面已经包含了依赖,可以直接 java -jar。

Dockerfile

在项目根目录新建一个 Dockerfile:

FROM eclipse-temurin:17-jre

WORKDIR /app

COPY target/demo-0.0.1-SNAPSHOT.jar app.jar

ENV TZ=Asia/Shanghai

EXPOSE 8080

ENTRYPOINT ["java", "-Duser.timezone=Asia/Shanghai", "-jar", "/app/app.jar"]

几行配置的含义:

  • 基础镜像用 JRE 而不是 JDK,体积小很多,运行应用不需要编译器;
  • WORKDIR 之后的相对路径都以 /app 为基准;
  • COPY 把本地 jar 复制进镜像,路径是相对构建上下文(docker build 最后那个 .);
  • ENV TZ 设置容器时区,配合 JVM 参数双保险;
  • ENTRYPOINT 用数组形式,避免 shell 模式下 Java 进程收不到停止信号。

如果项目用 JDK 8,基础镜像换成 eclipse-temurin:8-jre 即可,写法不变。

再加一个 .dockerignore,把 target、.git、logs、.idea 排除掉。构建上下文越小,docker build 越快,也避免把本地配置和密钥打进镜像。

镜像是一层层只读层叠加出来的,Dockerfile 里每条指令基本对应一层。把不常变的部分放前面、常变的部分放后面,能充分利用构建缓存。比如先复制 pom.xml 下载依赖,再复制源码,改代码时就不用重新下依赖。

更进一步可以用多阶段构建:第一个阶段用 Maven 镜像编译打包,第二个阶段只把 jar 复制到 JRE 镜像里。最终镜像不包含源码和 Maven,体积能小一半以上。

基础镜像的标签尽量写死小版本或 digest,别用 latest。今天构建和下周构建拿到的基础镜像可能不一样,出了问题很难复现。

构建与运行

  # 构建镜像
docker build -t demo-app:1.0 .

  # 后台运行,宿主 8080 映射到容器 8080
docker run -d --name demo-app -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  demo-app:1.0

  # 看日志
docker logs -f demo-app

  # 进容器排查
docker exec -it demo-app sh

-e 注入的环境变量会覆盖 Spring Boot 配置,比把生产密码写进镜像安全得多。日志用 docker logs 看,容器里的标准输出会被 Docker 收集,不用再进容器翻文件。

常见坑

1. 端口没映射

docker ps
  # 0.0.0.0:8080->8080/tcp 才是正常的

只写 EXPOSE 8080 不会把端口暴露到宿主机,-p 8080:8080 才生效。宿主机端口被占用时换一个:-p 8081:8080。

2. 容器里的 localhost 不是宿主机

Spring Boot 连本机 MySQL,配置写 localhost:3306,在容器里指向的是容器自己,必然连不上。Docker Desktop 可以用 host.docker.internal;Linux 上需要用宿主机 IP,或者启动时加 --add-host=host.docker.internal:host-gateway。正式环境则应该用 MySQL 容器的服务名或独立数据库地址。

3. jar 名写死

COPY target/demo-0.0.1-SNAPSHOT.jar app.jar 在版本号变更后就构建失败。可以改成通配符 COPY target/*.jar app.jar,但要确认 target 下只有一个 jar,否则会报错。更稳妥的做法是在 pom.xml 里用 <finalName> 固定产物名。

4. 时区差 8 小时

容器默认时区通常是 UTC,日志时间和页面时间差 8 小时。ENV TZ=Asia/Shanghai 加上 JVM 的 -Duser.timezone 基本能解决;如果用的是 eclipse-temurin 这类精简镜像,可能还需要安装 tzdata。

5. 镜像太大、构建太慢

把整个项目目录都 COPY 进镜像,会把 target、.git、node_modules 一起带进去。写一个 .dockerignore 排除这些目录,只复制 jar。进一步优化可以用多阶段构建,把 Maven 构建也放进 Dockerfile,但入门阶段先跑通再优化。

排查步骤

  1. docker ps -a 看容器状态,Exited 的话先 docker logs 容器名 看启动报错;
  2. 启动正常但访问不了:docker port 容器名 确认端口映射,再 curl http://localhost:8080;
  3. 连不上数据库:进容器 ping 或 nc -zv 目标地址,确认网络模式;
  4. 时间不对:docker exec 容器名 date 看容器时间,对比宿主机;
  5. 镜像构建失败:看 COPY 的路径是否在构建上下文内,.dockerignore 有没有把 jar 排除掉。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者