木杉笔记

记录后端、运维与数据库的踩坑日常

把镜像从 1.2G 压到 80M

2026-08-15容器

一个 Go 服务的镜像,初始版本 1.2G。压到 80M 花了大概两个下午,中间走了一些弯路。

先看每层占了多少

别急着换基础镜像,先看清楚体积花在哪儿:

docker history --no-trunc your-image:tag
dive your-image:tag

当时的结果很典型:编译工具链 600M,源码和缓存 300M,剩下一堆 apt 装的调试工具。

多阶段构建解决了大头

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app .

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

这样最终镜像里只有编译产物和运行时依赖。编译工具链全部留在 build 阶段,不进最终镜像。

-ldflags="-s -w" 去掉符号表和调试信息,一个中等规模的 Go 程序能少几 MB。CGO_ENABLED=0 让它变成静态链接,才能放进 distroless 这种连 libc 都没有的镜像。

层缓存的问题

把 COPY go.mod go.sum 和 RUN go mod download 放在 COPY . . 之前,是为了让依赖下载这一层独立缓存。不然每次改一行代码都要重新拉一遍依赖。

关于 alpine

很多人第一反应是换 alpine。但如果你用的是 CGO,alpine 的 musl libc 和 glibc 行为不完全一样,踩过 DNS 解析超时的坑。

我的结论是:Go 静态编译的用 distroless 或 scratch,需要 glibc 的用 debian:slim。alpine 的优势主要是体积,但代价是兼容性排查成本。

最后

加 .dockerignore 也是必须的。.git 目录、本地编译产物、测试数据,这些东西被 COPY . . 打包进去的话,白白增加体积还泄露信息。