Java 项目部署之 Docker工具快速入门: 自定义镜像的分层结构与写时复制
概述自己写的 Spring Boot 服务DockerHub 上不会有人替你构建镜像必须自己来。而要自己构建先得搞清楚一个镜像内部到底是怎么堆起来的——这一篇只讲结构不讲语法。纲要从镜像是什么这句话里拆出组成与顺序为什么必须是分层MySQL 5.7 升级到 5.8 的对比镜像的两端BaseImage 基础镜像层 与 Entrypoint 入口层逐行拆解真实 Dockerfile一条指令一层四层怎么叠出来只读分层与写时复制容器最上面那层可写层从哪来层复用的两个收益多镜像共享 base 省磁盘、构建缓存加速动手验证docker history看分层docker inspect看RootFS.Layers实战坑RUN 合并、missing的含义、改前一行废掉后面全部缓存与「Dockerfile 语法」一篇的边界镜像的组成回到那句话本身镜像是把应用程序及其需要的系统函数库、环境、配置、依赖打包到一起形成的文件。这句话听了很多遍但镜像的结构其实就藏在里面。系统函数库、环境、配置、依赖、应用是它的组成而这些组成之间不是平铺的是有依赖顺序的没有最底层的系统函数库和文件系统环境变量无处可配没有环境变量依赖装不对路径依赖没装好应用跑不起来应用装好了不给入口别人不知道怎么启动所以镜像不是把一堆文件揉成一团而是按依赖顺序分层构建。下面这段树就是构建一个 Java 镜像时真正放在一个目录里的东西docker-demo/ ├── Dockerfile # 构建描述文件9 条指令 ├── docker-demo.jar # Spring Boot 可执行 jar约 25 MB └── jdk8.tar.gz # 离线 JDK 安装包约 177 MBdocker build时 Docker 会读这个目录按 Dockerfile 一路往上叠层。为什么必须分层一个 5.7 到 5.8 的例子如果镜像真的是一疙瘩那你构建完 MySQL 5.7 镜像后想出一个 5.8 版本只能从最底层的文件系统开始重装环境、重配依赖、重装应用全程重来一遍。原来那坨东西你没法拆也就没法复用。分层之后情况完全不同。构建 5.7 时就有意把每一步的产物单独放一层① 基础文件系统 layer② 环境变量 / 运行环境 layer③ 依赖安装 layer④ MySQL 5.7 安装 layer⑤ 配置与入口 layer要出 5.8只需把 ④⑤ 两层砍掉在 ①②③ 之上重新叠。前面三层的构建时间、磁盘占用直接省下来。这就是分层的第一层动机复用。你构建自定义镜像时也应该从基础开始逐层构建别一把梭。镜像的两端BaseImage 与 Entrypoint一个常见镜像里两层是几乎必然存在的层名称作用是否必有最底层BaseImage 基础镜像层镜像依赖的最基础东西系统函数库、文件系统。像大楼地基必有最顶层Entrypoint 入口层应用启动的命令脚本告诉别人怎么把镜像跑起来除非这个镜像压根不打算被启动中间若干层构建产生的 layer环境变量、依赖安装、应用安装、配置文件……取决于构建时做了什么操作不确定做一次操作就多一层中间部分为什么不确定因为别人构建镜像时中间做了哪些动作你从外面看不出来。但有一条铁律每执行一次操作就产生新的一层。这个层就是 Layer。逐行拆解真实 Dockerfile讲义里给的 Ubuntu 方案是最能说明指令 层的素材。把它整段贴出来# 指定基础镜像 FROM ubuntu:16.04 # 配置环境变量JDK的安装目录 ENV JAVA_DIR/usr/local # 拷贝jdk和java项目的包 COPY ./jdk8.tar.gz $JAVA_DIR/ COPY ./docker-demo.jar /tmp/app.jar # 安装JDK RUN cd $JAVA_DIR \ tar -xf ./jdk8.tar.gz \ mv ./jdk1.8.0_144 ./java8 # 配置环境变量 ENV JAVA_HOME$JAVA_DIR/java8 ENV PATH$PATH:$JAVA_HOME/bin # 暴露端口 EXPOSE 8090 # 入口java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar从结构上看这 9 条指令对应四段职责自下而上叠成这样Layer 1 · BaseImageFROM ubuntu:16.04系统函数库 文件系统Layer 2 · 环境准备ENV JAVA_DIR COPY 两个包jdk8.tar.gz / docker-demo.jarLayer 3 · 安装运行时RUN 解压 JDK 并重命名 java8ENV JAVA_HOME / PATHLayer 4 · 应用入口EXPOSE 8090 ENTRYPOINTjava -jar /tmp/app.jar容器可写层运行时才有不属于镜像逐条对照看得更清楚Dockerfile 指令产生的一层干了什么为什么要单独一层FROM ubuntu:16.04拉来 Ubuntu 16.04 的只读文件系统作为地基换个 base 就是另一个镜像必须独立ENV JAVA_DIR/usr/local把 JDK 安装目录写成环境变量后续所有指令都引用它改一处不用改全文COPY ./jdk8.tar.gz $JAVA_DIR/把 177 MB 的 JDK 包拷进镜像大文件独立成层内容不变时永久命中缓存COPY ./docker-demo.jar /tmp/app.jar把业务 jar 拷进镜像业务代码改动最频繁让它尽量靠上别拖累下层RUN cd $JAVA_DIR tar -xf ... mv ...解压 JDK重命名为java8安装动作产生文件变更天然是一层ENV JAVA_HOME$JAVA_DIR/java8指向 JDK 根目录运行时依赖的环境变量ENV PATH$PATH:$JAVA_HOME/bin把java命令加进 PATH同上入口脚本才能直接调javaEXPOSE 8090声明容器监听 8090只是元数据但同样记一层ENTRYPOINT java -jar /tmp/app.jar定义启动命令行镜像的入口别人docker run时就靠它注意RUN里用了把三条命令连成一条——cd、解压、重命名如果拆成三个RUN就会多出两层。这一点后面实战坑里还会细说。只读分层与写时复制镜像的每一层都是只读的谁都不能改。那容器运行时产生的日志、临时文件、数据库数据写哪儿去Docker 的做法是启动容器时在镜像最上面再加一个可写层。容器读文件时从最上层往下找找到就用写文件时如果目标在下面的只读层里就把那个文件先复制到可写层再改下层保持原样不动。这就是写时复制Copy-on-Write。可写层没有下层只读本就在可写层读 /tmp/app.jar从最上层往下找Layer 4 只读Layer 3 只读Layer 2 只读Layer 1 ubuntu 只读命中返回文件写文件目标层可写?复制到可写层再修改下层原文件不动直接修改这个设计带来两个直接后果同一个镜像可以同时跑 N 个容器大家共用下面所有只读层只有最上面各自一个薄薄的可写层。省的是磁盘也是内存里共享的文件页。容器删掉只删它自己的可写层镜像本身毫发无损。所以容器不能持久化数据这个坑得靠数据卷挂载来解决。层复用的两个收益分层的价值不止是好看实测下来主要吃两头红利。一、多个镜像共享 base 层省磁盘。你构建的docker-demo:1.0和官方ubuntu:16.04共用同一批底层 layerDocker 在本地只存一份场景磁盘占用说明不分层每个镜像打包成独立文件各存各的177 MB 的 JDK 在每个镜像里各一份拉 5 个镜像就多 5 份分层相同 layer 共享base 层与未变化的中间层只存一份差异层单独存docker images里看到的 SIZE 是叠加值实际占用更小二、构建缓存加速改一行只重建它之后的层。构建时 Docker 会逐层比对某层的内容没变就直接复用缓存你改了什么受影响的层重建耗时只改业务 jar 里的一个类只有COPY ./docker-demo.jar之后的两层秒级前面的 JDK 解压全部命中缓存改了ENV JAVA_DIR的值从该ENV起的全部后续层解压 JDK 的RUN会重跑几十秒换了基础镜像FROM全部从零重建这解释了一个反直觉的实践把变化频率高的东西尽量放在 Dockerfile 靠后的位置。业务 jar 天天变JDK 几乎不变就别让业务 jar 的 COPY 排在 JDK 解压前面。动手验证把分层看出来光看图不放心跑两条命令自己看。用docker build构建完后先看分层历史dockerbuild-tdocker-demo:1.0.dockerhistorydocker-demo:1.0输出形如截取关键列IMAGE CREATED CREATED BY SIZE a1b2c3d4e5f6 2 minutes ago /bin/sh -c #(nop) ENTRYPOINT [java -jar… 0B 9f8e7d6c5b4a 2 minutes ago /bin/sh -c #(nop) EXPOSE 8090 0B 7c6b5a4d3e2f 2 minutes ago /bin/sh -c #(nop) ENV PATH/usr/local/… 0B 5d4c3b2a1908 2 minutes ago /bin/sh -c #(nop) ENV JAVA_HOME/usr/… 0B 3b2a1908f7e6 2 minutes ago /bin/sh -c cd /usr/local tar -xf ./jdk8… 175MB 1a0f9e8d7c6b 2 minutes ago /bin/sh -c #(nop) COPY file:… in /tmp/app.jar 25.4MB f0e9d8c7b6a5 2 minutes ago /bin/sh -c #(nop) COPY file:… in /usr/local/ 177MB e8d7c6b5a4f3 2 minutes ago /bin/sh -c #(nop) ENV JAVA_DIR/usr/local 0B c7b6a5f4e3d2 2 weeks ago /bin/sh -c #(nop) CMD [/bin/bash] 0B missing 2 weeks ago /bin/sh -c mkdir -p /run/systemd echo do… 7B三处细节值得盯#(nop)表示这条指令不产生文件变化比如ENV、EXPOSE、ENTRYPOINTSIZE 是0B但它仍然是一层。JDK 那层显示的是解压后的 175 MB而不是压缩包的 177 MB说明它记录的是该层的净变化量。missing出现的地方是 ubuntu 官方镜像构建时留下的中间层本地没有那一层的元数据常见于早年非 BuildKit 构建的镜像只能看到大小点不进详情。这不是报错。再看镜像的 RootFS能直接读到层 ID 列表dockerinspect docker-demo:1.0输出是一大坨 JSON只看RootFS部分RootFS:{Type:layers,Layers:[sha256:2a1b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809,sha256:3b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a,sha256:4c3d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b,sha256:5d4e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c]}Layers数组的长度就是层数顺序自下而上第一个元素是最底层的 base最后一个离容器可写层最近。两个镜像如果Layers前缀相同说明它们共享了这些底层。想只取这一段可以dockerinspect-f{{.RootFS.Layers}}docker-demo:1.0实战坑坑一层数堆太多体积膨胀。每层都会带来额外的元数据开销更麻烦的是——在某一层删掉的文件如果是在更下层加进来的删除只是记录一条删除标记文件本体依然存在下层里体积根本降不下来。典型写法# 反面教材三个 RUN三层且临时文件只被标记删除 RUN apt-get update -y RUN apt-get install -y vim net-tools RUN apt-get clean正确做法是合并成一条RUN让安装和清理发生在同一层# 推荐一条 RUN 搞定临时文件在同一层的末尾就没了 RUN apt-get update -y \ apt-get install -y --no-install-recommends vim net-tools \ rm -rf /var/lib/apt/lists/*坑二改 Dockerfile 前面某一行后面全部缓存失效。比如你把ENV JAVA_DIR/usr/local改成/opt那么它之后所有层的缓存 key 都变了Docker 只能从头往下重跑重新拷贝 jdk 包、重新解压、重新拷 jar。这就是为什么固定不变的东西往上放天天变的东西往下放不是风格偏好而是实打实的构建时间。坑三docker history里全是missing。用 BuildKit 或者拉取别人构建的镜像时很常见表示本地拿不到那层的构建指令元数据。它不影响运行只是看不到这一层是哪条命令产生的。想看完整信息自己构建时加--no-cache重跑或者用docker history --no-trunc打全CREATED BY。和下一篇的边界这篇只回答一个问题层是怎么来的为什么这么叠。至于 Dockerfile 里FROM、COPY、RUN、ENV、ENTRYPOINT各自有哪些参数、ADD和COPY有什么区别、CMD和ENTRYPOINT怎么配合——那是语法层面的东西留给「自定义镜像-Dockerfile」一篇。这里只需要记住一句每一个指令都会形成一层 Layer中间部分不确定两头一定是 BaseImage 和 Entrypoint。API 速览命令看什么关键点docker build -t 名称:版本 .从 Dockerfile 构建镜像末尾的.是构建上下文目录docker history 镜像逐层列出指令、大小、创建时间0B#(nop)表示元数据层missing是缺元数据非报错docker history --no-trunc 镜像完整显示CREATED BY指令被截断时用docker inspect -f {{.RootFS.Layers}} 镜像直接打印层 ID 列表顺序自下而上前几项相同即共享 basedocker image prune清理无标签的悬空层只删没被引用的不影响在用镜像官方文档Dockerfile referenceAbout storage drivers — 镜像层与写时复制总结镜像的分层不是设计美学是被三个现实问题逼出来的多版本之间要能复用、多个镜像之间要能共享、构建要能走缓存。记住三条就够用了一切从 BaseImage 开始往上叠一层只做一件事最上面永远有一个 Entrypoint。每层只读容器启动时在最顶上挂一个可写层写操作先复制再改。变化越频繁的内容在 Dockerfile 里放得越靠后构建缓存命中率才高。下一篇动真格用 Dockerfile 把一个 Java 项目真的构建成镜像跑起来。

相关新闻

mcporter 在 Windows 与 WSL 下的实战指南:绕过 NTFS/DrvFs 限制、稳定跑通测试与 CLI 生成

mcporter 在 Windows 与 WSL 下的实战指南:绕过 NTFS/DrvFs 限制、稳定跑通测试与 CLI 生成

【免费下载链接】mcporter Call MCPs via TypeScript, masquerading as simple TypeScript API. Or package them as cli. 项目地址: https://gitcode.com/gh_mirrors/mc/mcporter 点击查看 免费下载 本文是 mcporter 项目在 Windows 与 WSL(Windows Su…

2026/10/13 7:04:56 阅读更多 →
工业互联网技术体系落地指南:从分层架构到五大避坑经验

工业互联网技术体系落地指南:从分层架构到五大避坑经验

简介:《工业互联网-工业互联网技术体系.pptx》是一份聚焦工业互联网整体架构的演示文稿,面向工业互联网初学者、相关专业师生及企业技术人员,以“技术—生命”为主线梳理制造技术、信息技术与融合技术如何共同构成体系,并支撑制造…

2026/10/13 7:04:56 阅读更多 →
PS5 M.2 SSD扩容与硬件维护:兼容性、散热与排坑实战

PS5 M.2 SSD扩容与硬件维护:兼容性、散热与排坑实战

上个月,朋友搬来一台 PS5,说系统一直提示“无法使用此 M.2 SSD”。我把他那块在电脑上跑得好好的 PCIe 4.0 固态装进自己的机器里,格式化后居然一切正常。这个反差让我意识到,官方兼容列表其实只是“保证可用”的下限,…

2026/10/13 7:04:56 阅读更多 →

最新新闻

SpringBoot连接Hive数仓:HiveServer2 JDBC配置与排查实战

SpringBoot连接Hive数仓:HiveServer2 JDBC配置与排查实战

说句实话,第一次接到让 SpringBoot 应用连上 Hive 数仓的活,我内心的想法是:数据库连接嘛,换一个 driver,配一下 url,写两个查询方法,撑死了半天搞定。结果从环境准备到第一个查询真正跑通&…

2026/10/13 7:49:14 阅读更多 →
Ubuntu安装配置MySQL实战:版本选择、安全加固与性能调优

Ubuntu安装配置MySQL实战:版本选择、安全加固与性能调优

搞过 Linux 服务器的人基本都躲不开 MySQL,尤其是 Ubuntu 上安装配置这块。我最早是在一台配置很一般的云服务器上手搓,当时照着网上的文章一路 apt install mysql-server ,装完发现字符集乱码、root 密码没法远程登、端口被防火墙拦&#…

2026/10/13 7:49:14 阅读更多 →
仿QQ即时通信项目实战:Java Socket多线程全链路解析

仿QQ即时通信项目实战:Java Socket多线程全链路解析

很多找我推荐练手项目的人,都问过同一个问题:管理系统做腻了,有没有一个项目能让我把Java核心知识全部串起来,做完还能产生"我写的软件真的能跑起来"的成就感?我一般都会推荐仿QQ的即时通信项目。原因很简单…

2026/10/13 7:49:14 阅读更多 →
图像融合评估指标详解:从PSNR到SSIM的Python实现与避坑指南

图像融合评估指标详解:从PSNR到SSIM的Python实现与避坑指南

简介:面向图像融合算法研究的Python评估指标工具包,实现了EN、SF、SD、PSNR、MSE、MI、VIF、AG、CC、SCD、Qabf、SSIM、MS-SSIM、Nabf等十余种主流融合质量指标,支持单幅图像、单算法全部结果批量评估,也可一键计算所有对比算法并…

2026/10/13 7:49:14 阅读更多 →
Windows下MySQL安装配置全攻略:从环境准备到卸载清理

Windows下MySQL安装配置全攻略:从环境准备到卸载清理

1. 安装前的准备:版本选择与环境检查 1.1 先搞清楚该装哪个版本 提起MySQL在Windows上的安装,很多人第一反应是去官网点“Download”,结果面对一堆版本号直接懵了。这里我直接就事论事:目前主流选择是 MySQL Community Server 8…

2026/10/13 7:49:14 阅读更多 →
PhotoImpact v13 兼容指南:在 Win10/11 与虚拟机中重跑老图像软件

PhotoImpact v13 兼容指南:在 Win10/11 与虚拟机中重跑老图像软件

简介:Ulead PhotoImpact 13.0版是一套功能完整的图像编辑与设计软件,主要面向网页设计师、平面爱好者以及需要快速处理图片的办公用户。软件在图像制作、编修、克隆修复、滤镜填充、快速特效模板、抓图与动画方面都有完善表现,可高效产出网页…

2026/10/13 7:48:13 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →