年底整理项目时,我发现一个服务的镜像已经涨到一千多兆。它功能不复杂,拉取速度却像在搬家,临时扩容时尤其磨人。
打开镜像一看,编译器、源码、缓存和测试文件全住在里面。它们陪着程序完成构建后,不但没下车,还一路跟到了生产环境。
把行李留在构建阶段
我把镜像改成了分阶段构建。前一阶段负责干脏活,后一阶段只带走可执行文件:
FROM golang:1.24 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
改完以后,镜像体积一下轻了不少,发布等待时间也短了。更让我安心的是,生产环境里不再躺着一整套编译工具。
我还顺手整理了 .dockerignore。以前本地缓存和版本库元数据也会进入构建上下文,虽然未必出现在最终镜像里,却会拖慢上传,还经常让缓存莫名失效。
依赖文件放在源码前复制,是另一个很朴素的小改动。只要依赖没变,重新构建就不用从头下载。基础镜像和依赖版本也都固定下来,免得同一份代码隔几天构建出不同结果。
最后把运行用户换成非高权限用户,只留下证书、时区和程序真正需要的文件。镜像小并不是唯一目标,我更喜欢那种“打开箱子,每样东西都说得清用途”的感觉。