Dockerfile 为什么需要多个 FROM?与提前编译相比有什么优势?
可以先在 Docker 外编译应用,再用只有一个 FROM 的 Dockerfile 打包。如果运行阶段使用相同的基础镜像,并放入相同的文件,这两种方案的最终镜像体积可以相当。
多阶段构建的主要价值是把编译环境和运行镜像的制作过程统一写进 Dockerfile,同时让编译工具、源码和构建缓存留在构建阶段。
多个 FROM 在示例中做了什么?
问题中的 Dockerfile 包含两个阶段:
FROM golang:1.22 AS builder
第一个阶段提供 Go 编译环境,并在 /app/myapp 生成可执行文件。
FROM alpine:latest AS final
这一行以 Alpine 为基础,开始一个独立阶段。它不会自动继承前一个阶段安装的工具或复制进去的源码。
随后,这条指令从构建阶段复制指定文件:
COPY --from=builder /app/myapp .
正常构建该 Dockerfile 时,最终镜像包含 Alpine、安装的 ca-certificates 和应用程序等运行所需内容,不包含前一个阶段的完整 Go SDK 和源码。构建过程仍可能在本地或构建器中留下缓存。
只有一个 FROM 也能生成精简镜像
如果本地或 CI 已经具备合适的 Go 环境,可以先在项目根目录执行:
CGO_ENABLED=0 GOOS=linux go build -o myapp .
然后使用以下 Dockerfile:
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY myapp .
CMD ["./myapp"]
再执行:
docker build -t myapp .
这样也能让最终镜像只携带运行所需的文件。
上述编译命令适用于支持 CGO_ENABLED=0 的项目,还需要确保生成文件的 CPU 架构与运行镜像匹配。GOOS=linux 只指定操作系统。跨架构构建时,还需要设置合适的 GOARCH,并让镜像的目标平台与它一致。
多阶段构建有什么实际优势?
编译环境随项目一起定义
使用外部编译方案时,开发机器或 CI 需要自行准备 Go 工具链和其他构建依赖,工具链版本、环境变量和编译命令也需要由其他配置管理。
使用多阶段构建时,Dockerfile 可以同时描述:
- 使用哪个构建镜像。
- 如何获取依赖和编译程序。
- 哪些产物需要进入运行镜像。
在具备 Docker 构建环境和必要网络、凭据的前提下,开发者或 CI 可以通过同一个构建入口完成这些步骤,减少额外的环境准备工作。
这不能自动保证构建结果完全可复现。基础镜像标签、依赖内容和外部下载仍会影响结果。示例中的 alpine:latest 就没有固定到某个不可变的镜像内容。
构建和打包之间的交接更直接
外部编译也是可靠方案,但需要保证打包时使用的是刚生成、平台正确的产物。例如,如果编译失败后流水线仍继续执行,构建目录中遗留的旧 myapp 就可能被打包。
多阶段构建把编译与复制产物连接起来:构建阶段失败,依赖其产物的最终阶段就无法正常完成。外部流水线也能通过失败即停止、清理输出目录和明确传递产物,达到同样效果。
可以利用构建缓存
原示例先复制依赖清单:
COPY go.mod go.sum ./
RUN go mod download
COPY . .
这样可以让依赖下载步骤独立于普通源码变更。在基础镜像、依赖清单和其他相关缓存条件不变,且缓存可用时,修改业务代码通常可以复用依赖下载层。
缓存并非多阶段构建独有。多阶段构建的便利之处在于,编译和打包可以共用一套 Docker 构建流程。
为什么不直接在一个 Go 镜像里编译后删除工具?
如果唯一的基础镜像是 golang:1.22,最终镜像仍然以它为基础。这与“外部编译后复制到 Alpine”是不同的方案。后续层删除 Go 工具链或源码,不会移除较早镜像层中已经存在的数据,因此通常达不到多阶段构建的体积效果。
同一个 RUN 中临时下载的文件,如果在指令结束前删除,可以不进入该层最终的文件系统差异。但这不能消除基础镜像本身已有的内容。
多阶段构建更容易明确控制最终镜像包含什么,也省去了逐项清理编译工具和临时文件的工作。
应该选择哪一种?
如果 CI 已经统一管理编译环境,并能可靠地产出、校验和传递二进制文件,使用单个 FROM 打包很合适。
如果希望项目通过一个 Dockerfile 完成从源码到运行镜像的全过程,多阶段构建通常更方便。
两种方案都要检查运行依赖。示例安装 ca-certificates,是为了提供访问 HTTPS 服务时常用的 CA 证书;应用也可能需要时区数据、配置文件或其他资源。精简镜像有助于减少不必要的组件,但镜像小并不等于自动安全,仍需维护依赖和选择合适的运行权限。
备注:内容仅供参考。
