镜像体积瘦身实战:把一个 800MB 的 Docker 镜像压到 60MB
为什么镜像体积值得操心
上周我帮一个同学排查“部署慢”的问题:每次 CI 构建完,镜像要十几分钟才能在生产环境拉下来启动。看了一眼镜像,830MB——跑的只是一个 Go 写的小服务,典型的“小服务,大镜像”。
镜像体积直接影响三件事:
- 拉取和启动时间:镜像每大 100MB,跨机房拉取时就是实打实的多几分钟。
- 存储和流量成本:仓库保留多个版本,镜像大 10 倍,存储和流量账单就多几倍。
- 攻击面:臃肿的基础镜像里塞满了你用不到的工具包,每一个都是潜在的安全漏洞来源。
先量体温:找出体积的源头
动手之前先诊断。docker images 看总体积,docker history <image> 看每一层吃了多少:
docker images | grep myapp
docker history myapp:latest --no-trunc | sort -k4 -h -r | head -20
多数情况下罪魁祸首排前三:基础镜像本身(动辄几百 MB)、构建工具链(gcc、npm、maven 被一起打包进最终镜像)、以及每一条 RUN 指令留下的冗余层。
实战一:换一个小而够用的基础镜像
最立竿见影的一步。对比常见的 Python 基础镜像:
| 基础镜像 | 大约体积 |
|---|---|
| python:3.12 | 约 1GB |
| python:3.12-slim | 约 150MB |
| python:3.12-alpine | 约 60MB |
从 python:3.12 换成 python:3.12-slim,一行改动,体积直接掉 80%。alpine 更小,但它是 musl libc,如果依赖里有需要编译的 C 扩展,可能会踩坑——我的建议是:先用 slim,跑通后再试 alpine。
实战二:多阶段构建,把工具链留在构建阶段
这是 Go、Java、Node 项目最常用的手法。构建阶段用完整镜像编译,最终镜像只拿产物:
# 构建阶段:工具链都在这里,用完即弃
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/server ./cmd/server
# 运行阶段:只带一个二进制文件
FROM alpine:3.20
RUN adduser -D appuser
USER appuser
COPY --from=builder /app/server /app/server
ENTRYPOINT ["/app/server"]
-ldflags="-s -w" 去掉符号表和调试信息,又能再省几 MB。多阶段构建之后,最终镜像里连 go 编译器都没有,只剩你的二进制文件和 alpine 底座。
实战三:合并 RUN 指令,顺手清掉缓存
Docker 每一层都是增量存储。apt-get update 和 apt-get install 如果写在不同的 RUN 里,下载的软件包列表会被永久留在镜像层里。标准写法是:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
三个要点:同一条 RUN 里完成“更新—安装—清理”;加上 --no-install-recommends 跳过推荐但非必需的包;最后删掉 /var/lib/apt/lists/*。光是这一条习惯,很多镜像能省下几十 MB。
实战四:.dockerignore,COPY 之前先过滤
COPY . . 会把 .git 目录、本地日志、测试数据、node_modules 统统打进去。在项目根目录放一个 .dockerignore:
.git
.gitignore
*.log
.env
node_modules
__pycache__
tests/
它和 .gitignore 语法几乎一样,但作用于构建上下文。实测中,一个忘记写 .dockerignore 的前端项目,光 node_modules 就多带了 300MB 进镜像。
实战五:用工具验证,别靠感觉
推荐用 dive 看每一层的“浪费空间”:
dive myapp:latest
它会按层列出文件变化,标出哪些文件在某层被添加、又在后续层被删除——这类“先加后删”正是体积浪费的典型信号。CI 里还可以加一道门槛:构建后检查镜像体积,超过阈值就告警。
效果对比
还是开头那个 Go 小服务,优化前后:
| 优化步骤 | 镜像体积 |
|---|---|
| 原始(golang 完整镜像直接打包) | 830MB |
| 换 alpine + 多阶段构建 | 65MB |
| 加 -ldflags=“-s -w” | 58MB |
| 合并 RUN、加 .dockerignore | 55MB |
从 830MB 到 55MB,体积只有原来的约十五分之一,生产环境拉取时间从十几分钟降到几十秒。
避坑三条
- alpine 不是银弹:musl 与 glibc 的差异会导致某些二进制跑不起来,先在测试环境验证。
- 别为了小而删掉调试能力:生产镜像可以极简,但保留一个带 shell 的 debug 镜像标签,排障时会感谢自己。
- 体积和构建时间是 trade-off:多阶段构建加合并 RUN 会让 Dockerfile 可读性下降,团队内保持一致的写法规范更重要。
小结
镜像瘦身没有黑魔法,核心就是一句话:最终镜像里只放运行时真正需要的东西。换小基础镜像、多阶段构建、合并 RUN 清缓存、写好 .dockerignore、用 dive 验证——五步做完,大部分项目的镜像都能瘦掉一个数量级。今晚就可以挑一个自己的项目试试,先 docker history 看一眼,惊喜(吓)往往就在第一层。
- 点赞
- 收藏
- 关注作者
评论(0)