go多阶段构建可将镜像从900mb+压缩至12–20mb,核心是构建阶段用golang镜像编译,运行阶段仅复制静态二进制到scratch或alpine镜像,剔除sdk、缓存、源码等冗余内容。

Go 编译产物是静态二进制,不需要源码、依赖包或构建工具链;但若不清理中间文件,Docker 镜像会携带大量冗余内容——比如 go mod download 缓存、./vendor 目录、go build 生成的临时对象,甚至 git 元数据。这些不是“没用”,而是“不该进最终镜像”。
多阶段构建中必须删掉 go 工具链和源码
第一阶段(builder)装了 golang:1.21,第二阶段(runtime)却还保留 /usr/local/go 或 /go?这是最常见体积膨胀来源。别信“只 COPY 二进制就行”,Dockerfile 里漏掉 rm -rf /go 或未清空 /root/.cache/go-build,就等于把整个 Go 构建环境打包进生产镜像。
- 务必在 builder 阶段末尾显式清理:
rm -rf /go/pkg /go/src /root/.cache/go-build - 不要依赖
go build -trimpath自动清理路径——它不删缓存,也不影响/go/pkg - 如果用了
go mod vendor,vendor 目录必须只在 builder 阶段存在;runtime 阶段连vendor/的目录结构都不该出现
docker build --no-cache 不等于清理构建缓存层
很多人以为加了 --no-cache 就能绕过旧缓存污染,其实它只跳过指令层复用,不删除已存在的中间镜像层。那些曾被 RUN go mod download 写入的层,只要没被 docker builder prune 清掉,就一直占着磁盘——而且下次构建时,即使不用,Docker 仍要扫描它们。
- CI 环境中建议每次构建前执行:
docker builder prune --all -f - 避免在 Dockerfile 中写
RUN go mod download && go build ...这种单行合并——它让缓存失效粒度变粗,导致整层重建;拆成两行更利于复用go mod download层 -
go mod download后立即rm -rf /go/pkg/mod/cache,否则 cache 会随 builder 镜像固化
忽略 .git 和调试文件比想象中更重要
Go 项目根目录下 .git/ 平均占 5–20MB,debug/ 或 internal/testdata 类目录可能更大。它们不会被 COPY . . 显式排除,除非你提前干预。
- 必须用
.dockerignore列出:.git、.gitignore、go.mod(如果 runtime 阶段不需要)、testdata/、examples/ - 别在 Dockerfile 里补救,比如
RUN rm -rf .git——这只会新增一层“删 .git”,实际体积没减,还多了一层元数据 - 检查最终镜像是否含敏感信息:
docker run --rm -it <image> find / -name ".git" 2>/dev/null</image>,有输出就说明.dockerignore没生效或路径写错
用 distroless 镜像替代 alpine 进一步瘦身
Alpine 虽小,但仍含 apk、sh、ssl-ca-bundle 等运行时非必需组件。Go 二进制本身不依赖 libc,用 gcr.io/distroless/static-debian12 或 scratch 更干净。
- 选
scratch前确认你的二进制是 CGO_ENABLED=0 编译的(默认行为),否则会因缺少 libc 报no such file or directory - 若需 HTTPS 请求,
distroless镜像已预置 CA 证书;scratch需手动 COPY/etc/ssl/certs/ca-certificates.crt(不推荐,破坏最小性) - 调试困难是最大代价:
scratch镜像里连ls都没有,上线前务必在alpine镜像中验证功能完整
真正难的不是“怎么删”,而是判断“哪些东西删了不影响运行,但又确实进了镜像”。很多团队卡在最后一层:以为 COPY 了 main 就完事,结果 docker history 显示 builder 阶段的 /go/pkg 层仍被隐式继承——那是因为某条 RUN 指令没做清理,而 Docker 不会自动 GC 未引用的中间层。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











