多阶段构建是减小生产镜像攻击面和体积最有效、最标准的做法,通过分离编译与运行环境,仅保留必需二进制文件,显著降低漏洞风险与镜像大小。

直接用多阶段构建,是减小生产镜像攻击面和体积最有效、最标准的做法。核心就一条:把编译环境和运行环境彻底分开,最终镜像里只放运行必需的二进制或资源文件,其余一概不带。
为什么能同时缩小攻击面和体积
攻击面和体积本质上是同一问题的两面——镜像里装的东西越少,可被利用的漏洞、工具、配置就越少,体积自然也越小。
- 传统单阶段构建会把编译器(如 gcc)、包管理器(如 apt、apk)、源码、模块缓存、调试工具全塞进最终镜像,这些全是潜在风险点
- 多阶段构建中,构建阶段可以照常用 golang:1.21 或 maven:3.8 这类“重”镜像;运行阶段则切换到 alpine、distroless 甚至 scratch,只复制编译好的产物
- 最终镜像不含 shell、不含包管理器、不含编译器,连 /bin/sh 都可能没有——攻击者连基本命令执行环境都没有
关键操作要点
不是写了两个 FROM 就算多阶段,细节决定效果:
-
给构建阶段起别名:用
FROM golang:1.21 AS builder明确标识构建阶段,后续才能精准复制 - 运行阶段选最小基础镜像:alpine(5MB)适合需调试的场景;distroless(2–6MB)无 shell,更安全;scratch(0MB)仅适用于完全静态链接的 Go 二进制
-
只复制必要文件:用
COPY --from=builder /app/app /,而不是 COPY 整个构建目录;避免复制 .git、test/、docs/ 等无关内容 -
Go 项目务必静态编译:加
CGO_ENABLED=0 GOOS=linux,否则运行阶段还得装 glibc,破坏精简性
典型安全与体积对比(以 Go 应用为例)
假设原始单阶段镜像基于 golang:1.21:
- 体积:约 950MB(含 Go 工具链 800MB + mod 缓存 200MB + 源码)
- 攻击面:包含 root shell、apt、gcc、git、完整 libc,易被横向移动或提权利用
- 多阶段后(builder + alpine):体积压至 12–15MB,无包管理器、无编译器、无源码
- 升级为 distroless 或 scratch:体积进一步压缩到 6MB 以内,且默认禁用 shell,大幅限制攻击者能力边界
额外加固建议
多阶段是基础,再加几条能让安全水位更高:
- 运行阶段用非 root 用户启动:
RUN adduser -u 1001 -D appuser && USER appuser - 关闭不必要的端口和服务,只暴露应用真正需要的端口(
EXPOSE仅作声明,实际靠容器网络策略控制) - 构建时加
--no-cache或合理使用.dockerignore排除敏感文件(.env、.git、*.log) - 对最终镜像做 SBOM(软件物料清单)扫描,确认无已知高危 CVE 组件











