2.3 镜像仓库分发与瘦身


2.3 镜像仓库分发与瘦身

摘要:镜像仓库按层做增量分发,tag 只是可变的指针。本节讲清 pull/push 的层协商机制、tag 与摘要的区别,然后进入实战:用多阶段构建与工具链把一个 1.2 GB 的应用镜像压到 80 MB 级别。

学习目标

  1. 解释 docker pull 时"Layer already exists"背后的层协商机制
  2. 说明 tag 与 digest 摘要的区别,并知道为什么生产该用摘要
  3. 用多阶段构建压缩镜像一个数量级
  4. 用 dive 或 history 定位镜像里的重量级目录

分发:层是最小传输单元

push 的时候 Docker 把每层压缩后逐层上传,并先问 registry"这层你有了吗":

docker push myrepo/demo:1

输出逐层显示推送进度。第二次 push 一个共享基础层的镜像时,你会看到:

latest: digest: sha256:9f3c...a1e size: 1570 aa11b2...: Layer already exists 7cd01f...: Pushed

基础层直接跳过,只有新层真正传输。pull 同理。这是分层带来的第二个红利:网络传输按层增量,团队里第二个人拉镜像时,大部分层已经在本地了。

tag 则要小心对待。demo:latest 不是一份内容,而是一个会漂移的指针:今天指向这份,维护者重新 push 后就指向另一份。生产环境拉 :latest 等于放弃了可追溯性。两个稳妥做法:用不可变的版本 tag(demo:1.4.2),或者更进一步用摘要锁定:

docker pull myrepo/demo@sha256:9f3c...a1e

摘要对内容寻址,位变了摘要必变。生产清单里用摘要锁镜像,是第 7 章供应链安全的第一块砖。

私有仓库的搭建不必从零开始,官方 registry 镜像一条命令即可起一个:

docker run -d -p 5000:5000 --restart=always --name registry registry:2 docker tag demo:1 localhost:5000/demo:1 docker push localhost:5000/demo:1

企业里更常用的是带权限、扫描、复制功能的 Harbor 一类方案,第 7 章安全层再回来。

瘦身实战:从 1.2 GB 到 80 MB

先看病灶。一个典型的 Python 应用镜像:

FROM python:3.12 COPY . /app RUN pip install -r /app/requirements.txt CMD ["python", "/app/server.py"]

docker images 一看,1.2 GB 左右。为什么?python:3.12 完整版基础镜像自带 350 MB 的 Debian 完整工具链——编译器、man 手册、doc 文档,你的应用一个都用不上。加上 pip 缓存、build 依赖,体积滚雪球。

第一刀:换 slim 基础镜像。 python:3.12-slim 砍掉工具链,只剩运行时必需品,镜像立刻掉到 150 MB 附近。代价是缺编译器,某些需要本地编译的包要临时装 build 工具,装完即卸。

第二刀:不留缓存与清单。 pip 加 --no-cache-dir,apt 装 --no-install-recommends 且删列表。

第三刀:多阶段构建。 这是威力最大的一刀,适合一切编译型语言。思想:构建环境与运行环境分离,构建阶段要大要全,运行阶段只拷产物。以 Go 为例:

# 构建阶段:带完整工具链,无所谓大 FROM golang:1.22 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server . # 运行阶段:只要产物和运行所需 FROM alpine:3.20 RUN adduser -D appuser COPY --from=builder /out/server /usr/local/bin/server USER appuser ENTRYPOINT ["server"]

golang 构建镜像 800 MB 以上,但最终镜像只有 alpine 底座加一个静态二进制,通常 15 到 25 MB。编译器、模块缓存、源码,全部留在构建阶段,永远不进最终镜像。

一个 Go 应用两种构建的体积对比

一个 Go 应用两种构建的体积对比

第四刀:检查遗漏。 工具不会说谎:

docker history demo:1

按 SIZE 排出每层的重量,最重的层一目了然。想看得更深,用 dive 这类镜像分析器,能逐层列出新增了哪些文件、哪些目录异常肥大。常见的意外增重元凶:误拷进镜像的测试数据集、没配 .dockerignore 的 node_modules、以及解压后忘了删的压缩包。

选基础镜像的权衡

基础镜像 典型体积 优点 代价
debian 全量 120 MB+ 兼容性最好,工具全 无谓体积
slim 变体 25 至 80 MB 平衡点,兼容好 需临时补编译工具
alpine 5 至 8 MB 极小 musl libc,个别二进制不兼容
distroless 2 至 25 MB 无 shell 无包管理,攻击面小 排查困难,没有 sh 可进

alpine 不是免费午餐:它用 musl 替代 glibc,某些依赖 glibc 特性的 Python 轮子或预编译二进制会出诡异问题。我的建议是:动态语言先用 slim 稳住,静态编译语言上 alpine 或 distroless,出过兼容性坑的团队退回 slim。

瘦身不是抠那几十 MB 磁盘,是缩短分发时间、缩小攻击面、逼迫你分清"运行需要什么"和"构建需要什么"。

关于国内拉取的现实问题

国内环境拉取境外仓库时常遇超时,工程上有三类应对:配置镜像加速器走中转;企业内建私有仓库并配置上游代理缓存,第一次拉取走外网、之后全部命中本地;以及在 CI 里做镜像预热,把常用基础镜像定时同步到内网仓库。三类方案不互斥,成熟团队通常是三者叠加。另一个现实问题是被墙内网络掩盖的供应链风险:加速器本质是第三方中转,生产镜像务必用摘要校验,确保拿到的内容与预期一致。

本节要点回顾

  • 分发按层增量:已有层跳过传输,Layer already exists 就是协商结果
  • tag 可漂移,摘要不可变:生产清单用摘要锁定镜像
  • 多阶段构建是瘦身王牌:构建阶段全要,运行阶段只要产物
  • history 与 dive 是显微镜:先测量再动刀,别猜
  • alpine 的 musl 是兼容性雷区:选型时先过一遍依赖清单

镜像层到此拆完。下一章向上走一层,看容器运行时的两台隔离引擎。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U