ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot + MySQL 8 Docker 打造实战:编排、配置与排障全记录

Spring Boot + MySQL 8 Docker 打造实战:编排、配置与排障全记录 最近帮朋友把一个 Spring Boot 2.7 MySQL 8 的内部系统迁移到 Docker 上折腾了两天中间踩了不少坑。网上很多教程只讲了怎么跑起来但对于为什么这么配、踩坑之后怎么排查讲得很少。这篇文章就把这次实践完整记录下来从目录结构设计到 docker-compose 编排再到启动之后的故障排查一步步说清楚。这篇文章适合谁看已经会用 Spring Boot 开发项目但对 Docker 部署还停留在照着别人的 Dockerfile 抄的阶段的读者或者是被 MySQL 8 容器连接问题、时区问题、数据持久化问题折磨过的朋友。我会把这次部署的核心思路、配置文件逐行解释、以及实测中遇到的坑都写出来你可以直接照着复制修改。1. 先把整个架构想清楚再动手刚开始用 Docker 部署的人最容易犯的错就是拿到就写 Dockerfile写到一半发现容器连不上数据库或者数据一重启就没了。其实部署之前花十分钟把架构理清楚后面能省掉大量调试时间。1.1 这次要部署的组件关系我们这次要部署的是两层结构一个 Spring Boot 应用容器一个 MySQL 8 容器。应用容器通过 JDBC 连接数据库容器两者通过 Docker 内部网络通信不需要暴露 MySQL 端口到宿主机。这个设计在 Docker 官方文档里叫多容器应用核心思路是每个容器只跑一个进程容器之间通过服务名互相访问。为什么这样设计举个例子你如果直接把 MySQL 端口映射到宿主机 3306那任何能访问这台服务器的人都能尝试连数据库。而放在 Docker 内网里只有同一个 Docker 网络里的容器才能通过 3306 端口访问它安全性高不少。另外Spring Boot 应用和 MySQL 分开容器以后单独扩缩容、单独升级版本都很方便。1.2 为什么我推荐用 docker-compose 而不是裸 docker run有人喜欢写一堆 docker run 命令来启动容器我一开始也这么干直到有一次重启服务器后要按顺序敲七八条命令还要牢记参数的时候才意识到该用 docker-compose 了。docker-compose 的核心价值是用一个 YAML 文件把多个容器的配置、网络、依赖关系全部描述清楚一条命令就能拉起整个环境。在这次部署里我用 docker-compose 解决了三个裸 docker run 很难处理的问题一是两个容器必须在同一个自定义网络里写 docker run 要手动创建网络再分别指定二是 MySQL 必须比 Spring Boot 先启动裸命令很难控制这个顺序三是配置文件和启动命令希望集中管理compose 文件天然就是配置中心。1.3 端口规划与目录结构这是我把整个项目搭起来之前的规划建议你先照着这个弄不要跳过。spring-boot-mysql-demo/ ├── docker-compose.yml # 编排文件定义两个服务 ├── mysql/ │ ├── conf/ │ │ └── my.cnf # MySQL 自定义配置 │ ├── init/ # 首次启动时自动执行的 SQL │ │ └── init.sql │ └── data/ # 数据持久化目录首次启动自动生成 ├── app/ │ ├── Dockerfile # Spring Boot 应用镜像构建文件 │ └── demo.jar # 打包好的 jar 包说到端口应用本身的 8080 端口要映射到宿主机让外部能访问。MySQL 的 3306 端口只存在于 Docker 内网里不映射到宿主机。这样的好处我上面说了避免数据库直接暴露。如果你确实需要本地的 Navicat 之类的工具连这个 MySQL可以在 compose 文件里临时把端口映射加上生产环境建议去掉。2. 环境准备先把 Docker 跑起来这个环节看起来没什么好说的但实际上很多人卡在这里。我重点讲几个实战中的细节。2.1 Docker Desktop 的虚拟化支持问题在 Windows 或 macOS 上装 Docker Desktop会碰到一个非常常见的报错Docker Desktop requires a newer Windows version 或者 Virtualization support not detected。前者一般是系统版本太旧后者是 Windows 的 Hyper-V / 虚拟化没开。我的建议是装之前先去 BIOS 里确认 CPU 虚拟化技术已开启然后在 Windows 功能里勾选 Hyper-V 和 Windows 虚拟机监控程序平台。如果你用的是 Windows 家庭版没有 Hyper-V可以通过命令行启用适用于 Linux 的 Windows 子系统和虚拟机平台再用 Docker Desktop 自带的 WSL 2 后端这样也能跑起来。Linux 上装 Docker 就不多说了官方一条 curl 脚本的事装完记得把当前用户加进 docker 组不然每条命令都要 sudo。2.2 镜像源配置Docker Hub 在国内访问速度有时候不太稳定拉一个几百兆的镜像可能要等很久。我自己第一次拉 mysql:8.0 的时候就等过将近十分钟。解决办法是给 Docker 配置镜像加速源。Docker Desktop 在 Settings - Docker Engine 里改 JSON 配置Linux 则在 /etc/docker/daemon.json 里写。{ registry-mirrors: [ https://docker.m.daocloud.io ] }配置完重启 Docker 再拉镜像速度会明显提升。这里提醒一下镜像加速源可能有更新以你实际能访问到的为准。这个是合规且正常的配置操作不是让你去折腾网络层面的东西别想歪。2.3 JDK、Maven、打包工具的准备镜像构建阶段需要用到基础镜像里面有 JDK 环境。但在本地开发机上仍然需要 JDK 和 Maven 来把 Spring Boot 项目打成 jar 包。我在这次实践中用的是 JDK 17 和 Maven 3.9Spring Boot 版本是 2.7.18。要注意的是Spring Boot 2.7 可以跑在 JDK 8 到 JDK 21 上但如果你的项目里用了 LombokJDK 版本要和本地编译环境匹配否则打包会报 annotation processor 的错误。建议本地编译环境和容器运行环境保持同一个大版本能省不少事。3. MySQL8 容器配置认证插件、时区、持久化的细节MySQL 8 和 MySQL 5.7 在容器化部署上有几个很关键的区别。如果你是从 5.7 升上来或者第一次用 MySQL 8 容器下面这些问题基本都会遇到。3.1 官方镜像怎么选Docker Hub 上 mysql 镜像的 tag 有很多种mysql:8.0、mysql:8.0.36、mysql:latest。生产环境我建议不要用 latest因为 MySQL 8 里的 minor 版本升级也可能带来配置项和行为的变化最好锁定一个具体的版本比如 mysql:8.0.36。我这里用的是 mysql:8.0写 compose 文件时你也可以指定具体的 patch 版本。另外还有一个选择是 mysql:8.0-oracle 还是 mysql:8.0-debian。Oracle 版基于 Oracle Linux体积稍大但更接近官方推荐Debian 版更小。除非你有特殊需求直接拉默认的 mysql:8.0 就行。3.2 最坑的一个问题认证插件 caching_sha2_passwordMySQL 8.0 默认的认证插件是 caching_sha2_password取代了 MySQL 5.7 时代的 mysql_native_password。问题来了如果你的 JDBC 驱动版本太老或者某些图形客户端不支持 caching_sha2_password连接的时候会报类似这样的错误Authentication plugin caching_sha2_password cannot be loaded这个问题的本质是MySQL 服务端要求用加密握手方式验证身份而老的客户端不认这个协议。解决办法有三个思路第一升级 JDBC 驱动。MySQL Connector/J 8.0 完全支持 caching_sha2_password如果你的 Spring Boot 项目用的是 8.0 及以上的驱动其实不用改任何配置。第二在创建用户时指定用 mysql_native_password。早期版本我这么干过但 MySQL 8.0 后期版本已经开始标记这个方法为 deprecated新项目不建议再走回头路。第三在 my.cnf 里把默认认证插件改回去。同样不推荐这只是拖延升级的时间。我这次实践的做法很简单用最新版的 mysql-connector-j8.0.33然后正常建用户啥都不改。如果你确实碰到老客户端连不上的问题建用户时加一句 IDENTIFIED WITH mysql_native_password BY 密码 也可以临时过渡但心里要清楚这只是权宜之计。3.3 my.cnf字符集、时区、连接数在容器里跑 MySQL配置文件的路径是 /etc/mysql/my.cnf或者 /etc/mysql/conf.d/ 下的 .cnf 文件。我习惯在宿主机建一个 mysql/conf/my.cnf挂载到容器里方便修改和备份。下面是我这次用的配置逐行说一下[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-time-zone08:00 max_connections200 default_authentication_plugincaching_sha2_passwordcharacter-set-server 和 collation-serverMySQL 8 官方默认已经是 utf8mb4但你显式写出来更保险。utf8mb4 支持 emoji 和完整 Unicode 字符集比老的 utf8 靠谱得多。default-time-zone08:00这个很关键。Docker 容器默认时区是 UTC如果你不设置MySQL 存的时间字段和北京时间差 8 小时。除了这里容器环境变量里也要设 TZAsia/Shanghai。max_connections默认值是 151如果应用并发连接多可以调大。但要注意容器里 max_connections 设太大宿主机可能扛不住需要结合服务器内存来定。3.4 数据持久化重启不丢数据是底线容器是无状态的如果你不把数据目录挂载到宿主机rm 掉容器之后数据库就啥都没了。所以必须用 volume 或 bind mount 把 /var/lib/mysql 持久化。我用的是 bind mount也就是把宿主机的 mysql/data 目录挂载到容器内。这样做的最大好处是数据结构直观备份直接拷贝目录就行。在 compose 文件里这样写volumes: - ./mysql/data:/var/lib/mysql有人会问那初始化脚本怎么办MySQL 官方镜像有一个机制如果挂载的数据目录是空的容器首次启动时会自动执行 /docker-entrypoint-initdb.d/ 目录下的 .sql 或 .sh 脚本。所以你只需要把 init.sql 放到 mysql/init/再挂载到 /docker-entrypoint-initdb.d/ 就行。不过有个细节要特别注意只有第一次启动时数据目录是空的它才会执行初始化脚本。如果以后你想改初始化脚本已经初始化的数据目录不会重新执行。所以测试阶段如果改了初始化 SQL要把 mysql/data 清空再重建容器。init.sql 里我写了建库建用户。为什么不在环境变量里直接指定 MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD因为环境变量方式更简单但如果你的业务库里有多张表、多个用户还是用 SQ L脚本更灵活。比如下面的例子CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS demo_user% IDENTIFIED BY demo_pass; GRANT ALL PRIVILEGES ON demo_db.* TO demo_user%; FLUSH PRIVILEGES;注意 % 表示允许从任意主机连接。在 Docker 内网环境下应用容器访问 MySQL 时来源 IP 是 Docker 分配的网段地址如果你限制得太死比如只允许 localhost应用容器是连不上的。所以在容器场景下要么用 %要么指定 Docker 网段。3.5 环境变量里的坑MYSQL_ROOT_PASSWORD 和 TZ在 compose 文件的 MySQL 服务里有几个环境变量是官方镜像定义好的直接可以用environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: demo_db MYSQL_USER: demo_user MYSQL_PASSWORD: demo_pass TZ: Asia/Shanghai第一次启动时官方脚本会用这些变量自动创建数据库和用户。但如果你同时在 init.sql 里也建了同样的库和用户那也不会报错因为 SQL 里我写了 IF NOT EXISTS。这里要提醒的是MYSQL_DATABASE 和 MYSQL_USER 只有在数据目录为空时才会生效如果你已有旧数据它们不会起作用。如果你遇到环境变量设了但数据库没建出来的情况多半就是数据目录里已经有过一次初始化数据了。4. Spring Boot 应用镜像Dockerfile 与构建细节数据库容器搞定了接下来是 Spring Boot 应用本身的镜像。这里也有好几个容易踩坑的地方。4.1 基础镜像选择full JDK 还是 JRESpring Boot 应用打出来的是可执行 jar里面有内嵌的 Tomcat运行只需要 JRE 就够了。基础镜像可以用 openjdk:17-jdk-alpine、eclipse-temurin:17-jre-alpine 这类。我这次用的是 eclipse-temurin:17-jre-jammy体积适中稳定性和 JDK 版本都比较新。为什么不直接用 openjdk:17-jdk-alpine因为 Alpine 系统用的是 musl libc某些 JDK 版本在 Alpine 上跑会有些奇怪的问题。Temurin 是 Eclipse 基金会维护的开源 JDK 发行版兼容性好而且基础镜像覆盖了 Ubuntu/Debian 体系出问题的概率更低。4.2 多阶段构建一次搞定编译和打包如果你的服务器上已经装好了 Maven 和 JDK直接本地打好 jar 再拷进镜像是最简单的。但更专业的做法是多阶段构建让 Docker 自己完成编译打包这样你的宿主只需要装 Docker 和代码不需要装 JDK/Maven。多阶段构建的 Dockerfile 大致长这样# 第一阶段编译 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY --frombuilder /build/target/demo-0.0.1-SNAPSHOT.jar ./app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar]这里有一个优化点先单独 COPY pom.xml 再执行 dependency:go-offline目的是把依赖下载这个耗时步骤单独做成一个镜像层。这样以后修改了源码但没改 pom.xml重新构建时 Maven 依赖层可以走缓存不用每次都重新下载依赖。我第一次写 Dockerfile 的时候是把整个项目 COPY 进去再一起编译的每次改一行代码就要重新下载一遍依赖那叫一个酸爽。4.3 JVM 参数与 Spring Boot 运行配置生产环境跑 Java 容器JVM 参数一定要认真设置。默认情况下 JVM 会按宿主机内存的一定比例分配堆内存在容器里这会导致内存占用过高。所以我在 ENTRYPOINT 里显式指定堆内存参数ENTRYPOINT [java, -Xms256m, -Xmx512m, -jar, /app/app.jar]-Xms256m 是最小堆内存-Xmx512m 是最大堆内存。具体数值要根据你的应用实际使用情况来定。可以通过docker stats观察容器的内存占用率再调整这两个参数。还有个经验是给 JVM 加-XX:UseG1GCG1 在大多数业务场景下表现比默认的 Parallel GC 更好尤其在响应时间敏感的接口上。如果你的应用主要做大量短生命周期对象G1 的混合回收策略能降低 GC 停顿。关于 Spring Boot 的配置我强烈建议把数据库连接信息放在环境变量里而不是硬编码在 application.yml 中。Spring Boot 自带环境变量解析SPRING_DATASOURCE_URL会自动映射到spring.datasource.url。在 compose 文件里传给容器这样同一份镜像可以在不同环境复用。4.4 精简镜像层的技巧Docker 镜像每一层都会增加体积写 Dockerfile 时要养成尽量合并 RUN 命令、及时清理临时文件的习惯。多阶段构建已经帮我们过滤掉了构建工具运行阶段镜像里只有 JRE 和 jar 包。如果你还想进一步压缩体积可以用 jlink 生成精简 JRE只包含 java.base、java.sql、java.logging 等必要模块。这一步属于进阶优化一般场景下用标准 JRE 镜像就够了。5. docker-compose 编排服务间通信与启动顺序到这里MySQL 镜像和 Spring Boot 镜像都有了接下来用 docker-compose.yml 把两个服务串起来。5.1 完整 compose 文件逐行拆解先看完整文件version: 3.8 services: mysql8: image: mysql:8.0 container_name: demo-mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: demo_db MYSQL_USER: demo_user MYSQL_PASSWORD: demo_pass TZ: Asia/Shanghai ports: - 3306:3306 volumes: - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf - ./mysql/data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci networks: - demo-net app: build: ./app container_name: demo-app restart: always depends_on: - mysql8 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql8:3306/demo_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue SPRING_DATASOURCE_USERNAME: demo_user SPRING_DATASOURCE_PASSWORD: demo_pass TZ: Asia/Shanghai ports: - 8080:8080 networks: - demo-net networks: demo-net: driver: bridge逐段解释version: 3.8compose 文件格式版本。Docker Engine 20.10 以上版本都支持低版本可能部分字段不支持注意匹配。services 下的 mysql8 和 app 是两个服务名。服务名很关键因为在 Docker 内网里服务名就是可解析的主机名。Spring Boot 的 JDBC 地址里写的是jdbc:mysql://mysql8:3306/demo_db这里mysql8就是 service 名称Docker 内置 DNS 会把它解析成 MySQL 容器的 IP。restart: always容器挂了自动重启。生产环境很有用但不建议无限重启。后面讲故障排查的时候再说怎么避免启动失败但一直重启的循环。ports: 3306:3306这次我特意把 MySQL 端口映射出来了方便本地用客户端连上去调试。生产环境请把这行注释掉。networks: demo-net两个服务加入同一个自定义 bridge 网络。自定义网络比默认的 bridge 网络多一个好处自动启用服务名 DNS 解析。5.2 depends_on 的真相它不能保证数据库真正就绪这是 compose 编排里最容易踩的坑。depends_on 只控制容器的启动顺序MySQL 容器启动了不代表 MySQL 服务已经就绪。Spring Boot 可能在 MySQL 还没完全初始化完的时候就开始连接然后报连接失败。解决思路有两个。一是给 MySQL 加上 healthcheck然后让 app 的 depends_on 条件变为 service_healthy。代码如下services: mysql8: healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -proot123456] interval: 10s timeout: 5s retries: 5然后 app 服务里改成app: depends_on: mysql8: condition: service_healthy这样 compose 会等 MySQL 健康检查通过后再启动 app虽然还是有点轮询的意味但它确实解决了启动顺序的随机性问题。第二个思路是在 Spring Boot 应用的配置里加大连接超时和重试次数。Spring Boot 2.7 里可以在 application.yml 中配置 HikariCP 的初始化连接重试spring: datasource: hikari: initialization-fail-timeout: 60000意思是初始化连接时如果失败最多等 60 秒。配合 depends_on 的 service_healthy基本不会出现应用启动报数据库连不上的问题。5.3 为什么 JDBC URL 里要加 allowPublicKeyRetrievaltrue这条参数平时容易被忽略但 MySQL 8 使用 caching_sha2_password 认证时如果连接没有加密且需要交换 RSA 公钥客户端就必须设置 allowPublicKeyRetrievaltrue否则会报 Public Key Retrieval is not allowed 错误。这个参数在网上被吐槽很多因为它和 useSSLfalse 搭配时理论上存在中间人攻击的风险。但我们的场景是 Docker 内网服务间通信流量在自定义 bridge 网络中暴露面相对可控。如果你对安全要求很高可以改用 SSL 连接那就不需要这个参数了。对于绝大多数内网部署场景这个参数加上就行。5.4 时区问题在 compose 里的双重保证时区问题我前面提过一次这里再强调因为它值得。MySQL 容器通过 TZAsia/Shanghai 设置了系统时区同时 my.cnf 里也设置了 default-time-zone08:00。Spring Boot 容器同样设了 TZAsia/ShanghaiJDBC URL 里还带 serverTimezoneAsia/Shanghai。有人觉得这是重复劳动但我在实际项目里遇到过只设容器 TZ、MySQL 里存的时间还是差的场景。多次保险才能确保数据库存的、程序读的、接口返回的全部是北京时间。6. 启动验证与常见问题排查配置写得再完美不跑一遍永远不知道哪里会出问题。启动、验证、排错这一段我用实际操作路径来讲。6.1 首次启动命令与验证顺序在项目根目录执行docker-compose up -d-d 表示后台运行。第一次执行会拉取镜像、构建应用镜像时间可能比较长耐心等。构建完成后用下面的命令看容器状态docker-compose ps如果两个容器都是 Up 状态先看 MySQL 的日志docker logs demo-mysql8正常情况下你会看到早期 MySQL 实例初始化相关的日志包括 Temporary server started、ready for connections 等。然后看应用日志docker logs -f demo-app-f 是 follow 模式持续输出。如果一切正常你应该能看到 Started DemoApplication in X seconds 的输出。最后从宿主机验证接口curl http://localhost:8080/actuator/health如果 Spring Boot 配了 actuator会返回{status:UP}。没有的话随便调你自己的接口也可以。6.2 我实测踩过的三个大坑第一个坑应用一直重启循环日志里全是 Communications link failure。我当时没有配 healthcheckdepends_on 只是保证 MySQL 容器先启动了但 MySQL 还需要 20 秒左右初始化。应用尝试连接时 MySQL 还没 ready于是启动失败。restart: always 又让它自动拉起再次失败循环往复。解决方式就是我上面写的加 healthcheck service_healthy 条件。另外可以临时把 restart 改成 no先手动启动排查逻辑问题。第二个坑时区差 8 小时。这个坑我不会忘记。当时 MySQL 容器起来之后我用 Navicat 直接连上查数据时间是对的。但接口返回给前端的时间差 8 小时。排查了半天才发现是 JDBC URL 里的 serverTimezone 没写驱动用了系统默认时区。而且 Spring Boot 的 Jackson 序列化 LocalDateTime 时也有独立的时区配置。如果你用 LocalDateTime建议在 application.yml 里加spring: jackson: time-zone: Asia/Shanghai以防 JDBC 层正确了JSON 序列化层又出错。第三个坑容器里访问宿主机的服务。如果 Spring Boot 容器里需要调用宿主机上的另一个服务比如某个老系统的接口不能用 localhost因为容器内的 localhost 是容器自己。Linux Docker 可以用 host.docker.internal 这个特殊域名访问宿主机Windows/macOS 的 Docker Desktop 默认就支持。这个坑常见于混合部署场景也写在这里给后来人提个醒。6.3 如何快速定位容器日志与查看实时状态容器化部署最大的好处之一就是日志统一。用docker logs查看输出配合--since可以看最近几分钟的日志docker logs --since 5m demo-app看内存和 CPU 使用情况docker stats这个命令会实时刷新所有容器的 CPU、内存、网络 IO 情况对于排查内存溢出特别有用。如果应用内存一直涨考虑是不是 -Xmx 设置过大或者代码里有内存泄漏。进入容器内部排查docker exec -it demo-app /bin/sh在容器里可以执行 ps、netstat 之类的命令不过精简运行镜像里可能没有这些命令你需要自己装或者在基础镜像里预置。Temurin 镜像自带了一些基本工具大部分排查够用。6.4 安全与规范的最后检查部署上线之前最后检查一遍MySQL root 密码不要用 root123456 这种弱口令最好是随机生成的强密码。生产环境不要映射 MySQL 端口到宿主机。application.yml 里的密码不要明文硬编码改成环境变量注入。数据备份策略要想清楚mysql/data 目录定期拷贝到异地或者用数据库自身备份工具导出。这些点不是 Docker 特有的但容器让服务的迁移和复制变得太容易了反而容易让人忽略最基础的安全问题。以我个人的经验每次部署完我都会花十分钟检查一下 compose 文件里有没有把敏感信息写死端口有没有不该暴露的。从长期来看这些检查省下来的麻烦远比那十分钟值钱。这次 Docker 部署 springbootmysql8 的实践就完整记录到这里。最后再分享一个小技巧如果你要迁移到一台新服务器不用重新装环境只要把整个项目目录拷过去执行 docker-compose up -d五分钟内服务就能在新机器上跑起来。这就是容器化部署最直观的价值。
返回列表