Docker 镜像从 1.2GB 瘦到 180MB:一次多阶段构建的实战复盘
上个月接手一个内部服务时,我发现它的生产镜像足足有 1.2GB。拉取一次要三分多钟,节点磁盘告警、滚动发布超时、冷启动慢,问题一个接一个。花了半天做镜像瘦身,最终压到 180MB,拉取时间降到 20 秒以内。这篇把过程完整复盘一遍,坑和结论都写清楚,希望能帮到同样被大镜像困扰的同学。
一、先搞清楚空间被谁吃掉了
不要凭感觉删文件,先量化。两条命令足够定位问题:docker history --no-trunc <镜像> 按层列出每条指令新增的体积,dive <镜像> 可以交互式查看每一层的文件变更。跑完就会发现,真正的业务产物往往只有几十 MB,剩下的全是构建期残留:Maven 的本地仓库、node_modules 里的 devDependencies、编译器与头文件、源码目录,以及包管理器缓存。
根本原因是单阶段构建把"构建环境"和"运行环境"塞进了同一个镜像。构建需要 JDK、gcc、npm、git,运行时却只需要一个 JRE 或 Node 运行时。把两者混在一起,等于每次上线都带着一整套工具箱出门。
二、多阶段构建:让构建产物坐"货梯"
Docker 17.05 之后支持的 FROM ... AS 语法,本质上是在一个 Dockerfile 里定义多个镜像阶段,只有最后一个阶段会成为最终镜像,前面的阶段只作为"构建车间"存在。跨阶段用 COPY --from= 搬运产物,其余内容全部丢弃。
# 阶段一:构建车间
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B -DskipTests package
# 阶段二:运行现场
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY --from=builder /src/target/app.jar ./app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java","-XX:MaxRAMPercentage=75","-jar","app.jar"]
这个写法带来两个额外好处。一是 --mount=type=cache 把 Maven 仓库挂成构建缓存,依赖没变时不会重复下载,本地二次构建从 4 分钟降到 40 秒;二是构建工具链彻底不进入运行镜像,攻击面同步缩小,镜像里不再有 git、gcc、curl 这些容易被利用的工具。
三、几个真正决定体积的细节
换基础镜像是收益最大的一步。JRE 从 eclipse-temurin:17-jre 换到 alpine 变体,底座从 280MB 降到 160MB;Go 和 Rust 项目甚至可以直接 FROM scratch 或 gcr.io/distroless/static,只留一个静态二进制。但 alpine 用的是 musl libc,遇到依赖 glibc 的原生库(比如某些 JDK 版本、Oracle 客户端)会直接崩,这时候 distroless 或 slim 版本更稳妥,别为了体积牺牲可运行性。
合并 RUN 指令能少掉一批中间层。典型反例是 apt-get update 和 apt-get install 分成两条 RUN,缓存一失效就会装到旧索引。正确做法是一条命令里完成更新、安装、清理:apt-get update && apt-get install -y --no-install-recommends xxx && rm -rf /var/lib/apt/lists/*。注意 --no-install-recommends 常常能省下上百 MB 的推荐包。
还有两个容易被忽略的点。一是 .dockerignore:没有它,COPY . . 会把 .git、node_modules、target、日志全部送进构建上下文,既慢又容易把敏感文件带进镜像。二是时区与字符集,alpine 默认是 UTC 且不带中文字体,需要在 Dockerfile 里显式装 tzdata 并设置 TZ=Asia/Shanghai,否则日志时间和导出报表会莫名其妙差八小时。
四、验收与回归
瘦身不是一次性动作,要能守住。我的做法是在 CI 里加一道体积门禁:构建完成后读取镜像大小,超过阈值(当前设 250MB)就让流水线失败,并附上 dive 的层级报告。同时把 docker history 的输出存成构建产物,方便对比哪次提交让体积突然上涨。
最后提醒一句:不要用 :latest,也不要在运行镜像里留 shell。镜像小只是结果,真正被解决的是发布速度、冷启动时间和安全基线这三件事。把 Dockerfile 当成代码来维护,定期回看每一层为什么存在,比任何一次性的清理都有效。
- 点赞
- 收藏
- 关注作者
评论(0)