打破教科书:为什么我们的 Docker 部署只需要上传 Jar 包?
在标准的云原生教程中,发布一个微服务通常意味着一套繁琐的流水线:
编写 Dockerfile → 构建镜像 (docker build) → 推送镜像仓库 → 拉取镜像 → 运行容器 (docker run)
然而,在我们的实际服务器环境里,日常的发版动作却精简到了极致:
替换物理机上的 .jar 包 → 执行 docker restart
没有镜像构建,没有长串的启动参数,这背后的底层逻辑究竟是什么?
一、解剖一条真实的 docker run 命令
我们先从"建房"的那一刻说起。以 ***-device-connect-lc 服务为例,它的完整启动命令如下:
docker run \
--name=***-device-connect-lc \
--memory 812m \
-e TZ="Asia/Shanghai" \
-itd \
-p 8095:8095 \
-p 17007:17007 \
-v /data/approot/yundian/xinjiapo:/home \
egymgmbh/jdk11-builder \
java -jar /home/device-connect/lc/device-connect-lc-start-0.0.1.jar \
--spring.config.location=file:/home/device-connect/lc/config/application.yml
下面逐一拆解每个参数的含义:
docker run
触发 Docker 创建并启动一个全新容器的命令。这是一次性的重负载操作,会将后续所有参数的配置永久写入 Docker 的容器配置表中,后续 start / restart 都会沿用这份配置。
--name=***-device-connect-lc
给容器起一个人类可读的名字,而不是让 Docker 自动分配一串随机 ID(如 a3f2c1d9e8b7)。
有了名字之后,所有后续操作都可以用名字替代容器 ID:
docker restart ***-device-connect-lc # 比 docker restart a3f2c1d9e8b7 直观得多
docker logs ***-device-connect-lc
docker stop ***-device-connect-lc
--memory 812m
为容器设置最大内存限制为 812 MB。
这是一道硬性防护线。当容器内的 Java 进程因内存泄漏或业务峰值导致内存暴涨时,Docker 会在触及 812m 上限后强制终止进程(OOM Kill),避免单个服务把整台物理机的内存全部耗尽,从而保护服务器上其他微服务的正常运行。
实战提示:如果服务日志中频繁出现容器异常退出,可以用
docker inspect ***-device-connect-lc | grep OOM检查是否是内存限制触发了 OOM。
-e TZ="Asia/Shanghai"
-e 是 --env 的缩写,用于向容器内部注入环境变量。
TZ 是 Linux 系统的标准时区环境变量。由于基础镜像 egymgmbh/jdk11-builder 默认时区通常是 UTC(协调世界时),不设置此项的话,服务内部的时间戳、日志时间会比北京时间少整整 8 个小时,给排查问题带来极大困扰。
设置 TZ="Asia/Shanghai" 后,容器内的时间与北京时间完全对齐。
-itd
这是三个独立参数的合并写法:
| 参数 | 全称 | 作用 |
|---|---|---|
-i |
--interactive |
保持标准输入(stdin)开启,使容器保持可交互状态 |
-t |
--tty |
分配一个伪终端(pseudo-TTY),让容器像真实的终端一样运行 |
-d |
--detach |
让容器在后台静默运行,不占用当前终端窗口 |
三者组合的实际效果:容器在后台持续运行,你关掉 SSH 连接后服务依然存活,同时又保留了必要的终端特性,以便用 docker exec -it <容器名> bash 随时进入容器内部调试。
-p 8095:8095 和 -p 17007:17007
-p 是端口映射(Port mapping),格式为 -p 物理机端口:容器内部端口。
容器是一个隔离的网络环境,外界默认无法访问容器内的端口。-p 就是在物理机和容器之间打通一条网络通道:
-p 8095:8095:外部访问物理机的8095端口 → 流量自动转发到容器内的8095端口(HTTP 业务接口)-p 17007:17007:物理机的17007端口 → 容器内的17007端口(WebSocket 或其他长连接端口)
如果不做此映射,即便容器内的 Java 服务正常监听 8095,外部的 Nginx 和其他微服务也无法与其通信。
-v /data/approot/yundian/xinjiapo:/home
-v 是目录挂载(Volume mount),格式为 -v 物理机绝对路径:容器内路径。
这是整套架构的核心秘密。
此参数将物理机上的 /data/approot/yundian/xinjiapo 目录,与容器内部的 /home 目录实时双向打通。两个路径实际上指向同一块磁盘空间:
物理机: /data/approot/yundian/xinjiapo/device-connect/lc/device-connect-lc-start-0.0.1.jar
↕ 实时同步,无延迟
容器内: /home/device-connect/lc/device-connect-lc-start-0.0.1.jar
正因如此,我们通过 SFTP 在物理机上替换 Jar 包,容器内部会立刻"看到"最新的文件,docker restart 后便能加载最新代码,整个过程不需要重新构建任何镜像。
egymgmbh/jdk11-builder
这是容器所使用的基础镜像名称。
它是一个只预装了 Java 11 运行环境的轻量级"空壳"镜像,内部没有任何我们的业务代码。所有微服务(***-user-center、***-charge-application、***-device-connect-lc 等)共用这同一个基础镜像,代码全部通过上面的 -v 挂载注入。
java -jar /home/device-connect/lc/device-connect-lc-start-0.0.1.jar
这是容器启动后立即执行的主进程命令,即 Java 标准的 Jar 包启动方式。
路径 /home/device-connect/lc/... 是容器内部的视角。由于 -v 挂载的存在,它实际读取的是物理机上 /data/approot/yundian/xinjiapo/device-connect/lc/... 下的文件。
--spring.config.location=file:/home/device-connect/lc/config/application.yml
这是传递给 Java 进程的 Spring Boot 启动参数,用于覆盖默认的配置文件加载路径。
Spring Boot 默认会加载 Jar 包内部打包的 application.yml。通过此参数,我们将配置文件的来源重定向到容器内(即物理机上)的外部文件:
/home/device-connect/lc/config/application.yml
这意味着修改数据库连接、Redis 地址、第三方 API 密钥等配置时,无需重新打包 Jar,直接编辑物理机上的 application.yml,docker restart 后立即生效。
二、揭开伪装:我们跑的其实是"空壳容器"
通过上面的拆解,不难发现:我们的架构彻底抛弃了"代码即镜像"的传统云原生思维,转而采用"空壳镜像 + 目录挂载"模式。
传统模式: 代码 → 打入镜像 → 推送镜像仓库 → 拉取 → 运行
我们的模式:镜像(纯 JDK 环境)+ 物理机目录挂载(Jar 包 + 配置)→ 运行
三、一次"建房",终身"拎包入住"
理解了 docker run 的含义,start / restart 的本质就一目了然了:
| 命令 | 类比 | 说明 |
|---|---|---|
docker run |
拿地建房 | 一次性操作,将端口、挂载、环境变量等配置永久写入 Docker |
docker start |
开门入住 | 按建房时的图纸,直接唤醒已存在的容器 |
docker restart |
关门再开门 | 停止后立即按原配置重启,用于加载新 Jar 包 |
容器一旦创建,哪怕服务器重启,容器依然以 Exited(休眠)状态存在于系统中。docker restart 时,Docker 自动翻出当初 run 时记录的全套配置,带着挂载的最新 Jar 包重新把服务拉起来。
四、极其务实的日常发版工作流
得益于这种架构,日常发版流程实现了极致精简:
① 本地 Maven 打包 → 生成新的 device-connect-lc-start-0.0.1.jar
② SFTP 上传,覆盖 /data/approot/yundian/xinjiapo/device-connect/lc/ 下的旧包
③ 终端执行:docker restart ***-device-connect-lc
重启瞬间,容器内的 JVM 进程读取到的就是物理机上刚刚替换的最新 Jar 包,无需构建镜像,无需操作任何镜像仓库。
五、架构反思
这种方案看似"土味",对于中小型敏捷开发团队却有三大核心优势:
1. 极低的运维门槛
不需要维护 Jenkins 流水线,不用写 Dockerfile,不用搭建私有镜像仓库。
2. 节省服务器资源
避免了每次发版产生数百兆的悬空镜像(<none>),磁盘空间零损耗。
3. 极致的调试效率
需要临时修改外部配置(如 application.yml 中的某个参数),直接在物理机上改完,docker restart 瞬间生效,连重新打包的步骤都省了。
结论:架构没有绝对的对错,只有最适合当前团队阶段的解法。理解了
docker run每一个参数背后的底层逻辑,看似神秘的docker restart也就成了信手拈来的神兵利器。
浙公网安备 33010602011771号