镜像过大的危害

很多团队在容器化初期不太关注镜像体积,一个 Spring Boot 应用镜像动辄 800MB-1.2GB,一个 Go 应用也常有 700MB+。镜像过大带来的问题远不止"占点磁盘":

  • 拉取慢,部署延迟高:在 CI/CD 流水线或弹性扩容场景下,节点需要先拉取镜像再启动容器。1GB 的镜像在百兆内网下载需要 80 秒以上,而 50MB 的镜像仅需 4 秒。对于 HPA 自动扩容场景,这意味着故障恢复窗口被拉长。
  • 安全攻击面大:基础镜像里包含了大量你根本用不到的系统工具(curlwgetgccbash 等)。攻击者一旦拿到容器 shell,这些工具就是横向移动的跳板。镜像越小,攻击面越窄。
  • 存储成本累积:一个镜像 1GB,每天构建 5 次、保留 30 天就是 150GB。10 个微服务就是 1.5TB。 Harbor / Registry 的存储成本和备份成本随之飙升。
  • 构建缓存效率低:大镜像的每一层都更大,构建缓存命中后的加载也更慢,拖慢整体 CI 流水线。

本文参考 Docker 官方多阶段构建文档

多阶段构建(Multi-stage Build)

为什么需要多阶段构建

传统 Dockerfile 的关键问题是:构建工具和运行时环境混在同一个镜像里

以 Go 应用为例,编译需要 go 工具链和 gcc,但运行时只需要一个二进制文件。如果用 golang:1.22 做基础镜像,最终镜像里会带上整个 Go SDK(约 800MB+),而你的应用二进制可能只有 15MB。

多阶段构建允许在一个 Dockerfile 中定义多个 FROM,每个 FROM 开始一个新阶段,最终镜像只保留最后一个阶段的内容。

多阶段构建语法

# ===== 阶段1:构建阶段 =====
FROM golang:1.22-alpine AS builder

WORKDIR /app

# 先复制依赖文件,利用层缓存
COPY go.mod go.sum ./
RUN go mod download

# 再复制源码
COPY . .

# 静态编译,禁用 CGO
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o app ./cmd/server

# ===== 阶段2:运行阶段 =====
FROM alpine:3.19

# 安装最小化的运行时依赖
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app
COPY --from=builder /app/app .

ENTRYPOINT ["/app/app"]

关键点:

  • AS builder 给阶段命名,后续通过 COPY --from=builder 引用
  • CGO_ENABLED=0 禁用 C 绑定,生成纯静态二进制,可以在 scratchalpine 上运行
  • -ldflags="-s -w" 去除调试信息和符号表,二进制体积再减 30%
  • ca-certificates 是调用 HTTPS 接口必备的根证书包

更多细节参考 Docker 多阶段构建官方文档

基础镜像选型

选对基础镜像,是镜像瘦身的第一步,也是影响最大的一步。

三种主流方案对比

方案体积包管理器调试能力安全性适用场景
alpine~5MBapk有 shell,可装工具中(musl 偶有兼容问题)通用轻量镜像
distroless~20MB无 shell,无法 exec高(无攻击工具)安全要求高的生产环境
scratch0MB完全无最高纯静态二进制(Go/Rust)

Alpine

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY app /app
ENTRYPOINT ["/app/app"]

Alpine 优势是体积小、有 shell 方便排查,但使用 musl libc 而非 glibc,部分依赖 CGO 的程序(如 SQLite)可能遇到兼容性问题。

Distroless

Google 维护的 distroless 镜像不包含 shell、包管理器和任何多余工具,只有应用运行所需的最小化运行时。

FROM golang:1.22 AS builder
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

注意:distroless 没有 shell,kubectl exec -it <pod> -- sh 会失败。排查问题时可以临时换 alpine 镜像,或使用 debug 镜像 gcr.io/distroless/static-debian12:debug

Scratch

FROM scratch
COPY --from=builder /app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]

scratch 是空镜像,体积为 0。适用于纯静态编译的 Go / Rust 应用。必须手动复制 CA 证书,否则 HTTPS 请求会报 x509: certificate signed by unknown authority

层缓存优化

Docker 每条指令(RUNCOPYADD)都会生成一个新层。如果某一层的内容没变,Docker 会复用缓存。一旦某层失效,它之后的所有层都会重新构建。

指令顺序原则

将变化频率低的指令放前面,变化频率高的放后面。

# ✗ 错误:COPY . . 在前,任何源码改动都导致 go mod download 缓存失效
COPY . .
RUN go mod download
RUN go build -o app .

# ✓ 正确:先复制依赖文件,再复制源码
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app .

这样当你只改了业务代码、没改依赖时,go mod download 层命中缓存,跳过下载。

.dockerignore

.dockerignore 的作用类似 .gitignore,排除不需要的文件,避免它们被 COPY . . 带入构建上下文。

# .dockerignore
.git
.gitignore
*.md
docker-compose*.yml
Dockerfile*
node_modules
dist
build
.env
.env.*
*.test.go
*_test.go

.dockerignore 不仅减小构建上下文体积(加速 docker build 的 context upload),还能避免 .env 等敏感文件意外打包进镜像。

镜像瘦身工具

dive:镜像分层分析

dive 是一个可视化分析 Docker 镜像每一层内容的工具,能直观看到哪些文件占用空间、哪些层有浪费。

# 安装
brew install dive

# 分析镜像
dive myapp:latest

dive 会显示镜像的每一层、该层新增/删除的文件、以及效率得分(efficiency score)。如果发现某层 apt-get install 后没有清理 /var/lib/apt/lists/*,dive 会标红提示。

# 在 CI 中检查镜像效率
dive myapp:latest --ci \
  --efficiency 0.9 \
  --wasted-bytes 20MB \
  --wasted-pct 5

docker-slim

docker-slim 通过运行时分析自动裁剪镜像。它会启动容器、监控行为、记录实际用到的文件,然后生成一个只包含必要文件的精简镜像。

# 安装
brew install docker-slim

# 分析并精简
docker-slim build --target myapp:latest --tag myapp:slim

# 对比结果
docker images myapp
# myapp    latest   876MB
# myapp    slim      18MB

docker-slim 对静态文件服务类应用效果极好,但对动态加载类应用(如反射加载类的 Java 应用)可能误删文件,需要配合 --include-path 手动指定保留路径。

实战:Go 应用从 876MB 优化到 18MB

初始版本(876MB)

很多团队最初写的 Dockerfile 长这样:

# Dockerfile.v1 — 初始版本
FROM golang:1.22

WORKDIR /app
COPY . .
RUN go build -o app ./cmd/server

CMD ["./app"]
$ docker build -t myapp:v1 -f Dockerfile.v1 .
$ docker images myapp:v1
myapp   v1   876MB

问题:用 golang:1.22(基于 Debian)做运行镜像,带上了完整 Go SDK、Debian 工具链、调试符号。

第二版:多阶段构建 + Alpine(126MB)

# Dockerfile.v2 — 多阶段 + alpine
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app ./cmd/server

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/app .
CMD ["./app"]
$ docker images myapp:v2
myapp   v2   126MB

从 876MB 降到 126MB,削减 85%。但还有空间——二进制还带着调试符号。

第三版:编译优化 + Scratch(18MB)

# Dockerfile.v3 — 最终版本
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .

# -s -w 去除调试符号和 DWARF 信息
# CGO_ENABLED=0 生成纯静态二进制
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o app ./cmd/server

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
$ docker images myapp:v3
myapp   v3   18MB

优化效果对比

版本基础镜像体积优化手段
v1golang:1.22876MB无优化
v2golang:alpine → alpine126MB多阶段构建 + alpine
v3golang:alpine → scratch18MB+ CGO禁用 + 符号裁剪
# 用 dive 验证最终镜像
$ dive myapp:v3
  Total Image size: 18 MB
  Potential wasted space: 0 B
  Image efficiency score: 100 %

最终镜像效率得分 100%,没有任何浪费层。

推送前最终检查

# 检查镜像中是否有残留的构建工具
$ docker run --rm myapp:v3 ls /usr/bin 2>/dev/null || echo "No /usr/bin (scratch 镜像无文件系统)"

# 检查是否有 .env 或密钥文件泄露
$ docker run --rm myapp:v3 find / -name "*.env" -o -name "*.key" 2>/dev/null

# 扫描镜像漏洞
$ trivy image myapp:v3

总结

镜像优化不是一次性工作,而应该纳入 CI/CD 流水线的常态化检查:

  1. 多阶段构建是基础——构建环境和运行环境分离,这是性价比最高的一步
  2. 选对基础镜像——Go/Rust 用 scratch 或 distroless,Python/Node 用 alpine 变体
  3. 层缓存优化——依赖文件先复制、源码后复制,配合 .dockerignore 减小上下文
  4. 编译参数调优——-ldflags="-s -w"CGO_ENABLED=0 能再减 30% 体积
  5. 工具辅助验证——dive 检查分层效率,docker-slim 自动裁剪,trivy 扫描漏洞

将镜像从 876MB 优化到 18MB,拉取时间从 70 秒降到 1.5 秒,存储成本降低 97%,安全漏洞数从 200+ 降到 0。这不是锦上添花,而是 SRE 的基本功。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Docker 官方多阶段构建文档 — Docker 文档团队,参考了Docker 官方多阶段构建文档相关内容
  2. debug 镜像 — GitHub 开源社区,参考了debug 镜像相关内容
  3. dive — GitHub 开源社区,参考了dive相关内容
  4. docker-slim — GitHub 开源社区,参考了docker-slim相关内容