多阶段构建告别镜像 Obesity本文基于 Docker 24.x BuildKit展示如何把 1GB 的镜像瘦身到 20MB。场景引入镜像胖到推不动上篇咱们优化了构建速度但构建完的镜像让我傻眼了$dockerimages|grepmy-app my-app latest abc1231.2GB1.2GB就一个简单的 Web 服务里面塞了啥Node 基础镜像~180MBnpm install的依赖node_modules占了 800MB构建工具gcc、python、make为了编译某些原生模块源码、测试文件、.git目录…这就像你搬家时把装修工具、建筑材料、设计图纸全塞进新房——能住人但到处都是垃圾。多阶段构建就是解决这个问题的大杀器。核心原理一个 Dockerfile多个 “FROM”传统 Dockerfile 只有一个FROM构建产物和工具全塞一起。多阶段构建允许你写多个FROM每个阶段是一个独立的构建环境最后只把需要的产物拷贝到最终镜像。类比餐厅后厨 vs 前厅摆盘想象你去高级餐厅吃饭后厨构建阶段锅碗瓢盆、厨师、食材、调料乱七八糟但功能齐全传菜口COPY --from只把做好的菜端出去前厅摆盘最终镜像精致的盘子、菜品看不到后厨的油烟多阶段构建就是这个逻辑后厨多乱都没关系客人生产环境只看到精致的成品。实战对比Go 应用的单阶段 vs 多阶段Go 是最能体现多阶段构建价值的语言之一因为编译后只有一个二进制文件。单阶段胖镜像约 1GBFROM golang:1.21 WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o myapp . EXPOSE 8080 CMD [./myapp]$dockerbuild-tmyapp-fat.$dockerimages|grepmyapp-fat myapp-fat latest1.05GB多阶段瘦镜像约 15MB# 阶段 1编译 FROM golang:1.21 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . # 静态编译不依赖系统库 RUN CGO_ENABLED0 GOOSlinux go build -a -installsuffix cgo -o myapp . # 阶段 2运行 FROM alpine:latest # 安全考虑用非 root 用户下篇细讲 RUN adduser -D -u 1000 appuser USER appuser WORKDIR /app # 只拷贝编译好的二进制文件 COPY --frombuilder /app/myapp . EXPOSE 8080 CMD [./myapp]$dockerbuild-tmyapp-slim.$dockerimages|grepmyapp myapp-slim latest15.2MB myapp-fat latest1.05GB从 1GB 到 15MB瘦了 98%而且最终镜像里没有 Go 编译器、没有源码、没有go.mod只有一个能跑的二进制。前端项目Node 构建 → Nginx serving前端项目同样适合多阶段用 Node 构建用 Nginx 托管静态文件# 阶段 1构建 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . RUN npm run build # 生成 dist/ 目录 # 阶段 2托管 FROM nginx:alpine # 只拷贝构建产物 COPY --frombuilder /app/dist /usr/share/nginx/html # 自定义 nginx 配置可选 COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]最终镜像基于 Nginx Alpine约 25MB没有 Node、没有node_modules、没有源码。Java 项目Maven 构建 → JRE 运行Java 也能大幅瘦身从 JDK 切换到 JRE甚至用自定义 JREJava 11 的jlink# 阶段 1编译 FROM maven:3.9-eclipse-temurin-17-alpine AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline # 缓存依赖 COPY src ./src RUN mvn clean package -DskipTests # 阶段 2运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 只拷贝 jar 包 COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]JDK 镜像约 400MBJRE 镜像约 150MB省了 60%。进阶极致瘦身——Distroless 和 ScratchDistrolessGoogle 出品极简但能用FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED0 go build -o myapp . # 没有 shell、没有包管理器只有运行必需的库 FROM gcr.io/distroless/static-debian12 COPY --frombuilder /app/myapp / CMD [/myapp]Distroless 镜像只有几十 MB而且没有 shell安全性极高攻击者进去啥命令都执行不了。Scratch从零开始Go 可以编译成完全静态链接的二进制连操作系统库都不依赖FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED0 go build -o myapp . # 空镜像啥都没有 FROM scratch COPY --frombuilder /app/myapp / # 需要拷贝 CA 证书才能发 HTTPS 请求 COPY --frombuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ CMD [/myapp]最终镜像不到 10MB这是 Docker 镜像的理论极限。⚠️坑Scratch 里没有sh调试极其困难。生产环境用 Distroless 更平衡。多阶段之间的数据传递COPY --from是跨阶段拷贝的关键# 从指定阶段拷贝 COPY --frombuilder /app/myapp . # 从指定镜像拷贝甚至不需要前面定义过 COPY --fromnginx:alpine /etc/nginx/nginx.conf /tmp/ # 从索引号拷贝第 0 个 FROM COPY --from0 /app/myapp .一句话总结多阶段构建就像餐厅后厨和前厅分离编译工具、源码留在构建阶段只把二进制产物带到最终镜像——体积能从 GB 压到 MB攻击面还小了一大圈。