为什么用 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,但入门阶段先跑通再优化。
排查步骤
docker ps -a看容器状态,Exited的话先docker logs 容器名看启动报错;- 启动正常但访问不了:
docker port 容器名确认端口映射,再curl http://localhost:8080; - 连不上数据库:进容器
ping或nc -zv目标地址,确认网络模式; - 时间不对:
docker exec 容器名 date看容器时间,对比宿主机; - 镜像构建失败:看
COPY的路径是否在构建上下文内,.dockerignore有没有把 jar 排除掉。
评论