摘要:镜像仓库按层做增量分发,tag 只是可变的指针。本节讲清 pull/push 的层协商机制、tag 与摘要的区别,然后进入实战:用多阶段构建与工具链把一个 1.2 GB 的应用镜像压到 80 MB 级别。
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 章安全层再回来。
先看病灶。一个典型的 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。编译器、模块缓存、源码,全部留在构建阶段,永远不进最终镜像。

第四刀:检查遗漏。 工具不会说谎:
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 就是协商结果镜像层到此拆完。下一章向上走一层,看容器运行时的两台隔离引擎。