一、从跑一个容器到跑一堆容器上篇学完我已经能把 hm-service 打成镜像、用docker run跑起来了。但黑马商城是微服务项目订单、商品、网关好几个服务外加 MySQL、Redis 这些中间件每个都手敲一遍docker run参数又长又容易抄错改一次配置就得全部重来。所以下篇解决两件事容器里的数据怎么不丢数据卷多个容器怎么一起管Compose最后把 hm-service 完整部署一遍。二、数据卷容器的外置硬盘最早起 MySQL 容器的时候忘了挂-v。中途 restart 了几次都没事数据还在我就以为没问题了。后来rm重跑库和表全没了。当时我盯着空荡荡的数据库脑子里只有一个想法我明明没删数据啊。愣了几秒才反应过来——数据一直在容器里容器没了数据就没了。restart 不会丢数据rm才会。这一点我后来才弄清楚restart 只是重启容器可写层还在rm才是把容器和它的可写层一起删掉。数据卷就是为这个场景准备的把宿主机目录挂载进容器dockerrun-vmysql-data:/var/lib/mysql mysql-v后面的格式是宿主机路径或卷名:容器路径。容器往这个路径里写的数据其实落到了宿主机上。容器删了数据还在重建一个容器挂同一个目录数据接着用。用法上大概分两类MySQL 数据目录、Redis 持久化文件这类必须保住的挂命名卷或固定目录配置文件、日志目录这类经常改的用绑定挂载宿主机上改完容器里立刻生效不用重新打镜像。命名卷和绑定挂载的区别也弄清楚了docker volume create出来的命名卷由 Docker 统一管理位置藏在/var/lib/docker/volumes下面省心但不好直接摸到绑定挂载就是自己指定宿主机路径位置透明。数据求稳用命名卷配置求好选用绑定挂载。从那以后起中间件之前我会先问自己一句这容器里有什么数据是不能丢的。三、Docker Compose用 YAML 管理多容器服务一多一个个docker run就撑不住了命令越敲越长启动顺序没法保证参数也没个记录。Compose 的思路很直接——把所有docker run收进一个docker-compose.ymlservices:hm-service:build:.ports:-8081:8080environment:-DB_HOSThost.docker.internaldepends_on:mysql:condition:service_healthyrestart:alwaysmysql:image:mysql:8volumes:-mysql-data:/var/lib/mysqlhealthcheck:test:[CMD,mysqladmin,ping]interval:5sretries:10volumes:mysql-data:我挑几个容易和docker run搞混的字段说一下services下面每个键就是一个容器image/build用现成镜像写image要从 Dockerfile 构建写buildports等价于-p“宿主机:容器”environment等价于-e塞环境变量depends_on声明启动顺序。默认只保证mysql 容器先启动配合condition: service_healthy才能真正等到 MySQL 就绪volumes数据卷用法和-v一样顶层的volumes用来声明命名卷restart容器挂了自动拉起always表示除非手动停否则一直重启healthcheck定期检查容器里的服务是否真的可用剩下的字段用到的时候查文档就行不用背。depends_on 只管启动不管就绪一开始只写了depends_on: - mysql以为这样 hm-service 就会等 MySQL 准备好了再启动。结果 hm-service 起来之后还是报连接失败——因为它只保证mysql 容器启动了不保证mysql 服务能连了。后来在 mysql 上加了 healthcheck再用condition: service_healthy才能真正等到 MySQL 可以接受连接了再启动 hm-servicedepends_on:mysql:condition:service_healthyhealthcheck 里的test是检查命令mysqladmin ping能通就说明 MySQL 活着。interval是检查间隔retries是失败重试次数超过次数 Docker 就认为容器不健康。还有一个点restart: always是生产部署的标配。学习阶段可能感觉不到它的价值但容器因为意外挂了、服务器重启了这行配置能让服务自动恢复不用人手动去拉。部署实战里这行配置比任何优化都实在。常用命令命令作用docker compose up -d后台启动所有服务docker compose down停止并删除容器默认保留命名卷docker compose logs -f实时查看日志Compose 不是另一个 Docker它只是把散落的 docker 命令文件化。yml 里每个字段几乎都能对应到上篇学过的某个命令。四、部署实战把 hm-service 跑起来这次把上篇零散学的东西串了一遍。先mvn package -DskipTests打出 jar然后基于openjdk:17-jdk-slim写 Dockerfiledocker build构建最后dockerrun-d--namehm-service-p8081:8080-eDB_HOSThost.docker.internal hm-service:1.0起容器。端口映射 8081:8080浏览器访问宿主机 8081流量转进容器的 8080。构建和启动都没出问题结果服务一起来就抛反射错误——MyBatis-Plus 序列化 lambda 表达式时反射访问 SerializedLambda抛出InaccessibleObjectExceptionCaused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.Class java.lang.invoke.SerializedLambda.capturingClass accessible: module java.base does not opens java.lang.invoke to unnamed module 27abe2cd at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at com.baomidou.mybatisplus.core.toolkit.SetAccessibleAction.run(SetAccessibleAction.java:18) at com.baomidou.mybatisplus.core.toolkit.SerializedLambdaMeta.clinit(SerializedLambdaMeta.java:19) at com.baomidou.mybatisplus.core.toolkit.LambdaUtils.extract(LambdaUtils.java:55)查了一下根源在 JDK 9 引入的模块系统JDK 16 之后强封装变成默认行为java.base不再对未命名模块开放java.lang.invoke这些内部包MyBatis-Plus 反射读 SerializedLambda 内部字段时正好被拦住。平时在本地 IDEA 里跑感觉不到IDE 往往自带各种宽容参数换成干净的容器就现原形了。报错信息里写清了是哪个包没开对着加--add-opens就行ENTRYPOINT [java, --add-opens, java.base/java.lang.invokeALL-UNNAMED, -jar, /app.jar]改完重新 build服务顺利起来。那一刻我有点后怕。如果不是容器逼着我把环境暴露出来我可能一直不知道自己的项目依赖了 IDE 的宽容参数。在我电脑上能跑这句话在容器面前毫无意义。五、小结下篇补上了数据卷、Compose、hm-service 部署实战外加一个印象很深的 JDK 17 反射坑。连同上篇的镜像、容器、Dockerfile 和网络Docker 基础部分到这里闭环了。两篇写完回头看Docker 最核心的东西其实就两个分层和隔离。镜像分层叠加容器各自隔离。Compose 管的是把这一堆隔离的东西组合起来数据卷管的是隔离边界上的持久化。上篇的镜像、容器、网络下篇的数据卷、Compose说到底都是围绕这两个词在转。难的真不是命令是理解背后的设计思想。命令忘了随时能查文档思想通了遇到没见过的报错也知道往哪儿查。