多阶段构建能减少层数,因其仅将最终产物从构建阶段复制到运行阶段,跳过中间层;copy –from 不继承源阶段层数,且运行阶段用 scratch 镜像可实现零层,但 alpine 更具调试实用性。

多阶段构建为什么能减少层数
docker 镜像层数不是由 RUN 指令数量决定的,而是由每条指令产生的文件系统快照叠加而成。传统单阶段构建中,编译工具、源码、临时文件即使在后续 RUN 中被 rm -rf 删除,仍会保留在某一层里,无法真正“消失”。多阶段构建通过只把最终需要的产物(比如二进制文件)从构建阶段 COPY --from=builder 到运行阶段,跳过中间层,让最终镜像只含运行时最小依赖。
- 构建阶段用
golang:1.22-alpine,运行阶段用alpine:3.20或scratch - 运行阶段基础镜像越小,最终镜像体积越小,层数也越少(
scratch是零层) -
COPY --from不会继承源阶段的层,只复制文件内容,这是层数不累积的关键
常见错误:COPY –from 用错阶段名或路径
最常遇到的报错是:failed to compute cache key: "/app" not found: not found,或者构建时提示 unknown stage name。这通常是因为:
- 阶段别名写错,比如定义的是
FROM golang:1.22 AS builder,但COPY --from=build少了个r -
COPY --from=0这种数字索引写法看似方便,但一旦调整FROM顺序就失效,强烈建议始终用显式别名 -
COPY --from=builder /workspace/app /app中源路径不存在,Go 编译默认输出在/workspace?其实默认在当前工作目录,得看WORKDIR和go build -o指定位置
Go 项目典型多阶段写法(避免 go mod cache 重复下载)
Go 编译本身很快,但反复 go mod download 会拖慢构建且增加层数。合理利用构建缓存和分层是关键:
- 第一阶段先
COPY go.mod go.sum .,再RUN go mod download,这样只要模块没变,后续构建直接复用该层 - 再
COPY . .,然后RUN CGO_ENABLED=0 go build -a -ldflags '-extldflags "-Static"' -o /app main.go - 运行阶段用
FROM alpine:3.20,COPY --from=builder /app /app,再ENTRYPOINT ["/app"] - 不要漏掉
CGO_ENABLED=0,否则 Alpine 上可能因缺少 glibc 而启动失败
Alpine vs scratch:选哪个更“少层”
scratch 确实是零层镜像,但代价是:没有 shell、没有 ls、没有 sh,连 docker run -it image sh 都进不去。调试时非常被动。
- 如果应用是静态编译的 Go 程序(
CGO_ENABLED=0),scratch安全可用 - 但只要有动态链接依赖(比如用了 cgo、sqlite、OpenSSL),就必须用
alpine或debian-slim -
alpine:3.20自身只有一层(基础镜像层),加上你 COPY 的二进制,通常就是两层 —— 已经足够干净
实际交付时,别为了“理论上更少层数”硬上 scratch,尤其当团队没统一调试流程时,一个 no such file or Directory 错误就得重推镜像查半天。层数少不是目的,镜像可靠、可维护、易排障才是。