CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Docker 容器入门/课时/镜像与分层
RawBlame
课时目录12 节

Docker 容器入门

  1. 01容器是什么
  2. 02安装与环境
  3. 03Docker CLI 基础
  4. 04镜像与分层
  5. 05编写 Dockerfile
  6. 06构建与优化
  7. 07镜像仓库
  8. 08数据卷与挂载
  9. 09网络
  10. 10Docker Compose
  11. 11容器安全基础
  12. 12综合练习与 K8s 方向

镜像与分层

镜像 vs 容器、Union FS、OCI。

30 分钟

镜像与分层

这一课你将能做到

  1. 亲手用 pull、images、history、inspect 四个命令把一个镜像「拆开看」,说出每一层是什么
  2. 用实验证明「镜像只读、容器可写」:改容器内文件、删除容器、重建后对比数据去向
  3. 说清 tag 与 digest 的区别,明白 mysql:8.0、8.0.40、latest 三者的真实关系

为什么要学

不理解分层,后面的课程全是黑盒:为什么 Dockerfile 指令顺序影响构建速度?为什么两个 Spring Boot 镜像只占一份 JRE 的磁盘?为什么 docker push 只推了 3MB?答案全在「分层」两个字上。

对 Java 后端更实际的意义:你的应用镜像 = 基础镜像(OS + JRE)+ 依赖层 + 你的代码层。理解结构,才能在第 6 课做构建优化、在第 7 课正确管理版本、在第 11 课评估安全风险(哪层引入了漏洞)。

先建立图像

镜像是「只读层的堆叠」,容器在此基础上加一个可写层:

           ┌─────────────────────────┐
           │  容器可写层(Writable)    │ ← docker exec 写的东西在这
           ├─────────────────────────┤
           │  app.jar 层   (只读)     │ ← 你 COPY 的代码
镜像 =     ├─────────────────────────┤
一叠只读层  │  依赖/配置层    (只读)    │
           ├─────────────────────────┤
           │  JRE 层        (只读)    │
           ├─────────────────────────┤
           │  Debian/Alpine 基础层(只读)│
           └─────────────────────────┘

三个关键机制:

  1. 层可复用:你的两个 Spring Boot 镜像都用 eclipse-temurin:21-jre,那 OS 层和 JRE 层磁盘上只存一份。
  2. 写时复制(CoW):容器要改只读层里的某个文件,先把文件复制到可写层再改,原层永远不动。
  3. 删除是「白名单遮蔽」:容器里删一个来自只读层的文件,只是在可写层标记删除,镜像里文件仍然存在。

跟着做

步骤 1:拉镜像,观察分层下载

docker pull mysql:8.0

输出类似:

db6fc19d3c9b: Pull complete
a39b63fa0f31: Pull complete
...
Digest: sha256:abc123...
Status: Downloaded newer image for mysql:8.0

每一行就是一个层。如果之前拉过共享基础镜像,会看到 Already exists——这就是层复用的直接证据。

步骤 2:查看本地镜像列表

docker images
REPOSITORY   TAG      IMAGE ID       CREATED        SIZE
mysql        8.0      f5da8161f17e   3 weeks ago    573MB
redis        7        1a35c5b13c8e   4 weeks ago    117MB

列含义:REPOSITORY+TAG 是「名字」,IMAGE ID 才是「本体」,SIZE 是解压后的逻辑大小(多层共享,实际磁盘占用小于各行相加)。

步骤 3:用 history 看每一层怎么来的

docker history mysql:8.0

每行对应镜像构建时的一个 Dockerfile 指令(RUN/COPY/ENV...),从下往上读就是构建顺序。以后你自己的镜像构建出问题, 是还原「我当时干了什么」的第一工具。

history

步骤 4:用 inspect 查镜像元数据

docker inspect mysql:8.0

重点字段:.Architecture(amd64 还是 arm64,跨机器迁移必看)、.Config.Env(默认环境变量)、.Config.ExposedPorts(声明了哪些端口)、.RootFS.Layers(层的 sha256 列表)。

步骤 5:实验——证明镜像只读、容器可写

# 起一个 redis 并进容器写个文件
docker run -d --name img-test redis:7
docker exec img-test sh -c "echo important > /data/proof.txt"
docker exec img-test cat /data/proof.txt     # 输出 important

# 停止、删除、用同一镜像重建
docker rm -f img-test
docker run -d --name img-test2 redis:7
docker exec img-test2 cat /data/proof.txt    # 报错:No such file

结论:容器可写层随 rm 销毁,镜像毫发无损。数据库数据放可写层 = 定时炸弹(第 8 课解决)。

步骤 6:tag 与 digest

docker tag redis:7 my-redis:backup        # 同一镜像多一个名字,IMAGE ID 不变
docker images | findstr redis             # 两个名字指向同一个 ID
docker rmi my-redis:backup                # 删的只是别名
docker images --digests                   # 查看 digest(内容哈希)

tag 是可变的别名(8.0 会被官方更新覆盖),digest(sha256:...)由镜像内容算出,不可变。生产环境要锁死版本,就用 digest 拉取。

步骤 7:清理无用镜像

docker rmi mysql:8.0                 # 删指定镜像(有容器在用会拒绝,先删容器)
docker image prune                   # 清理 <none> 的悬空镜像(dangling)
docker image prune -a                # 清理所有没被容器使用的镜像(慎用)

讲清楚要点

  1. 镜像 = 配置 + 只读层堆叠:不只是文件系统,还包含启动命令(Cmd/Entrypoint)、环境变量、暴露端口等元数据。所以「同一镜像」保证的不只是文件一致,还有默认行为一致。
  2. 层的复用单位是「内容哈希」:层内容不变,哈希不变,可被任意镜像共享。这是 push/pull 增量传输、构建缓存的基础,后面两课都建立在这上面。
  3. tag 是别名,digest 是身份证:latest 只是没有显式指定 tag 时的默认名,不代表最新版本,更不代表稳定。生产镜像引用要么精确到补丁号(mysql:8.0.40),要么直接用 digest。
  4. 镜像大小 ≠ 磁盘占用:docker images 的 SIZE 是各层解压后的独立大小;由于层共享,docker system df 才是真实磁盘账单。
  5. 架构必须匹配:Apple Silicon(arm64)默认拉 arm64 镜像,部署到 amd64 服务器会报 exec format error。跨架构要么在构建时指定 --platform linux/amd64,要么用 buildx 多架构构建(第 6 课)。

你可能会懵的地方

  • 为什么 pull 一个 MySQL 要下载那么多层? MySQL 镜像 = Debian 基础层 + 依赖库层 + MySQL 本体层……层层堆叠。而且这些层和别的官方镜像共享,拉过一次,其他镜像的对应层秒下。

  • 两个不同 tag 显示同一个 IMAGE ID,是 bug 吗? 不是。docker tag 只是加了个别名,底层数据完全共享。删除其中一个名字,另一个照常可用。

  • 容器里删除了一个文件,镜像会变大吗? 不会。删除只是可写层里的「标记」,镜像内容不变。但要明白:想真正去掉镜像里的文件,只能重新构建(且要在它所在层之后删才有意义——同层内删文件不减体积,这是第 6 课优化点)。

  • 8.0-alpine 和 8.0 有什么区别?该用哪个? alpine 版把基础系统换成 5MB 的 Alpine Linux,体积小很多;但它是 musl libc,个别依赖 glibc 的库会踩坑。MySQL/Redis 官方 alpine 版很成熟可放心用;Java 应用选 alpine 镜像需测试字体、时区等细节,稳妥选择是 slim/JRE 变体。

  • docker images 里一堆 <none>:<none> 是什么? 悬空镜像(dangling):重新构建同名 tag 后,旧镜像失去了名字但数据还在。docker image prune 一键清理。

错法对照

错法问题正解
生产环境引用 latest每次拉取版本可能不同,出问题无法复现固定到具体版本号或 digest
以为容器内改文件能「修正」镜像改动随容器删除而消失修改 Dockerfile 重新构建
数据库数据存容器可写层rm 容器即丢数据挂数据卷(第 8 课)
在 arm64 Mac 上构建后直接推给 amd64 服务器运行报 exec format error构建时 --platform linux/amd64
用 docker rmi 删镜像报「image is being used」有容器还挂着它先 docker rm 相关容器再删镜像
磁盘爆了才想起来清理dangling 镜像悄悄堆积定期 docker image prune

卡点急救

  1. pull 报 no matching manifest for linux/arm64:镜像不支持你的 CPU 架构。加 --platform linux/amd64 强制拉 amd64(会走模拟,性能略降)。
  2. docker rmi 提示多个 tag 混乱删不干净:按 IMAGE ID 删:docker rmi <image-id>,一次清掉所有别名。
  3. 怀疑本地镜像被污染/想重置:docker rmi 镜像名 后重新 docker pull;极端情况用 docker system prune -a(会删掉全部未使用镜像,先确认)。
  4. 两个镜像看起来一样但 ID 不同:构建时间或指令有差异,用 docker history 对比每层来源,定位差异层。
  5. 磁盘占用远超镜像列表总和:容器可写层、构建缓存、卷都在占空间。docker system df 分类查看,docker system prune 统一清理。

面试常问

  • 镜像分层的好处是什么? ①存储复用:多镜像共享基础层,省磁盘;②传输高效:pull/push 只传本地缺失的层;③构建缓存:指令未变的层直接复用,加速 CI;④升级最小化:只改代码层时,其余层原封不动。

  • 解释写时复制(Copy-on-Write)? 容器启动时不需要复制整个镜像,只在修改某文件时才把它从只读层复制到自己的可写层再修改。这让「同一镜像秒级起 N 个容器」成为可能。

  • tag 和 digest 的区别?生产环境怎么引用镜像? tag 是可变别名(可被重新指向),digest 是内容寻址的不可变标识(sha256)。生产环境推荐 镜像:具体版本 并在发布单里记录 digest;要绝对不可变就用 digest 直接拉取。

  • 容器删除后,写入的数据去哪了? 可写层随容器一起销毁,数据彻底消失。所以持久化数据必须放 volume——这是有状态服务容器化的前提。

自测三问

  1. 画图说明镜像的层结构与容器的可写层关系,并解释 CoW 过程。
  2. mysql:8.0 和 mysql:8.0.40 可能是同一镜像吗?latest 呢?如何验证?
  3. 为什么说「两个 Spring Boot 镜像各 300MB,但磁盘可能只增加 350MB」?

小结一句话

镜像是「配置 + 一叠内容寻址的只读层」,容器只是在其上多了一个可写层——分层带来复用与缓存,可写层的短命决定了持久化必须依赖数据卷。

延伸

  • Docker 镜像概念文档
  • docker image 命令参考
  • 下一课预告:编写 Dockerfile——亲手把你的 Spring Boot jar 打成镜像。
加载本节练习…
← 上一课Docker CLI 基础下一课 →编写 Dockerfile