第24章|容器基础 1:镜像、分层、构建与运行

容器最迷人的地方在于:它把“环境”从一坨不可描述的状态,压缩成了一个可复现的制品(镜像)。 但容器也最容易被误解:它不是小 VM;镜像不是压缩包;tag 也不是版本真相。 这一章我们把容器拆成三块:镜像与分层构建与缓存运行与隔离,以及交付里最关键的:tag vs digest

Core mental model

镜像 = 多层只读 + 一个可写层

  • 每条 Dockerfile 指令几乎都在产生 layer
  • 缓存命中取决于“layer 是否相同”
  • 运行时写入发生在容器的 writable layer
Isolation

容器隔离靠内核:namespace + cgroup

  • namespace:看起来像“独立世界”(进程、网络、挂载…)
  • cgroup:资源配额与限制(CPU/内存/IO)
  • 安全边界:容器不是强隔离边界(别当沙箱)
Delivery

tag 不可变?别信,信 digest

  • tag 是“人类友好指针”,可能被重打
  • digest 是内容地址(content-addressed),才是制品真相
  • 交付追溯:digest + SBOM + 签名

1. 镜像是什么?不是压缩包,而是“可复现文件系统快照”

镜像的直觉比喻是“分层千层饼”:底层是基础镜像(如 Debian/Alpine),上面一层层叠加你的依赖、构建产物与配置。 真正重要的点是:镜像层是只读的;容器运行时,会在最上面加一个可写层

直观结论:容器里“写文件”通常不应该当成持久化。你要么写到挂载卷(volume),要么写到外部存储(DB/对象存储),要么把状态交给平台。

2. 构建缓存:为什么 Dockerfile 的顺序能把 CI 从 30 分钟压到 5 分钟?

镜像构建的缓存命中是以 layer 为单位的:如果某一步的输入变化了(Dockerfile 指令、复制进来的文件、build args),这一层以及它之后的层都需要重建。 所以 Dockerfile 最关键的“工程技巧”之一是:把变化最频繁的东西尽量放在后面,把稳定的依赖安装放在前面,并用 lockfile 控制输入稳定性。

镜像分层与缓存:变化越靠上,重建越便宜 思路:稳定依赖在下层,频繁变更(业务代码)在上层;缓存命中按 layer 断点失效 FROM base RUN apt-get ... RUN pip/npm install (lock) COPY src/ ... RUN build/test Cache rule a layer is reusable if inputs unchanged COPY changes → invalidate from that layer upward lockfile stabilizes dependency layer inputs multi-stage reduces final image size cache hit boundary CI impact move stable steps earlier → more hits avoid copying whole repo too early use .dockerignore to shrink context
图 1:镜像分层与缓存命中。把“稳定输入”(依赖、系统包)放下层,把“高频变化”(业务代码)放上层,CI 时间会明显下降。

3. 运行与隔离:容器不是虚拟机,它是“被隔离的进程集合”

容器在 Linux 上的本质是:一组进程运行在同一个内核上,但通过 namespace 看到“不同的世界”,通过 cgroup 被限制资源。 它像一间带玻璃墙的办公室:看起来独立,但地基是同一栋楼(同一个内核)。

安全提醒:容器不是强安全边界。尤其是特权容器、挂载宿主机敏感路径、过宽的 Linux capabilities,都会把“玻璃墙”砸碎。
容器隔离边界:namespace 是“视图”,cgroup 是“额度” 同一内核上隔离进程:看起来像独立系统,但安全边界需要额外加固 Host kernel same kernel, different namespaces + cgroups Container A PID/NET/MNT namespaces cgroup: CPU 1 core · mem 512Mi processes fs view network stack Container B PID/NET/MNT namespaces cgroup: CPU 2 cores · mem 2Gi processes fs view network stack namespaces = view cgroups = quota
图 2:容器隔离边界。namespace 让你“看起来独立”,cgroup 让你“被限制资源”。它们让容器轻量,但不等于强隔离。

4. tag vs digest:为什么交付必须用 digest 才能“可信可追溯”?

tag 是方便人记的名字,比如 web:prodapp:1.2.3。 但 tag 可能被重打:同一个 tag 在不同时间指向不同内容。digest 则是内容地址(content-addressed),只要内容不变,digest 就不变。 所以在 CD/GitOps 中,“上线的版本”应该用 digest 表达,这样才可追溯、可回滚、可审计。

结论:tag 用于“选择与展示”,digest 用于“交付与审计”。上线用 digest,展示用 tag,不打架。
tag vs digest:指针 vs 真相(交付追溯链) 目标:把“上线的东西”绑定到内容地址(digest),并关联 SBOM/签名/Provenance Image registry tag: app:1.2.3 digest: sha256:… tag can move, digest cannot Traceability digest → SBOM → signature → provenance CD uses digest; dashboards may show tag rollback = switch digest to previous known-good Delivery chain (example) commit → build → image@sha256:... → sign → deploy (digest) → observe 每一步留下证据:谁批准、用什么凭证、交付到哪、发生了什么 实践建议:在 GitOps/Deploy manifests 里引用 image digest,而不是 tag。
图 3:tag vs digest 的交付追溯链。tag 可能漂移,digest 才是“上线的内容真相”。配合 SBOM/签名/Provenance 才能做到可信交付。

5. 最小落地清单(从“会用 Docker”到“能工程化交付”)

  1. Dockerfile 顺序优化:稳定层在下、易变层在上;锁文件稳定依赖层输入。
  2. 构建上下文控制:写好 .dockerignore,别把无关文件送进构建。
  3. 运行时无状态:把持久化写到 volume 或外部系统;容器重启应可恢复。
  4. 最小权限:非 root、最小 capabilities、避免特权与宿主敏感挂载。
  5. 交付用 digest:上线引用 digest;tag 用于展示;建立追溯链(SBOM/签名/审计)。
你现在应该能回答:
1) 为什么 Dockerfile 顺序会影响缓存与 CI 时间?你会怎么重排?
2) 容器隔离靠什么实现?它是不是强安全边界?为什么?
3) 上线版本应该用 tag 还是 digest 表达?如何做到可追溯与可回滚?
← 上一章:GitOps 入门 下一章:容器基础 2(镜像仓库、签名与供应链) →