Docker 镜像瘦身实战:多阶段构建把 1.2GB 应用镜像压到 120MB
一、1.2GB 的镜像到底贵在哪
我们组有个 Java 服务,镜像推上去 1.2GB。一开始没人管,直到两件事发生:一是每次发布流水线光拉取镜像就要三分多钟,节点磁盘三天两头告警;二是安全扫描报出一百多个 CVE,绝大多数来自构建阶段带进去的编译工具链,运行时根本用不到。
镜像体积不是一个"好看"指标,它直接换算成钱和时间:拉取时间、节点磁盘占用、镜像仓库存储、Pod 冷启动延迟,以及漏洞修复的工作量。这几项里,最后一项最疼。
二、先搞清楚镜像里有什么
不要凭感觉删。先看分层和目录占用:
docker history --no-trunc myapp:1.2.0 | head -20 docker run --rm -it --entrypoint sh myapp:1.2.0 du -sh /* 2>/dev/null | sort -h | tail -10
一般会看到三类东西:构建工具(maven、gcc、头文件)、包管理器缓存(/root/.m2、apt lists)、以及多份重复的依赖副本。我们的镜像里 /root/.m2 单独占了 480MB,src 目录还留着全量源码。
三、多阶段构建:把编译环境和运行环境切开
这是收益最大的一步。构建阶段用完整镜像,运行阶段只拿产物。
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /src COPY pom.xml . RUN mvn -B dependency:go-offline COPY src ./src RUN mvn -B clean package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY --from=build /src/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-XX:MaxRAMPercentage=75","-jar","app.jar"]
关键点是 COPY --from 只搬运行时要的东西。构建层的 maven、源码、.m2 全部留在中间镜像里,不进最终镜像。
四、换基础镜像:alpine 不是万能解
把 eclipse-temurin:17-jre-jammy(约 270MB)换成 alpine 版能再省一百多兆,但要先确认两件事:一是应用是否依赖 glibc 特有行为,musl 对 DNS 解析和 locale 的处理不同,我们有个服务在 alpine 上偶发 UnknownHostException;二是 native 依赖有没有预编译的 musl 版本,没有就得在镜像里装编译链,反而更大。
不想碰 alpine 的话,distroless 是另一个方向:没有 shell、没有包管理器,攻击面最小,代价是进容器排查只能靠日志和临时 debug 镜像。
五、合并 RUN、清缓存、写 .dockerignore
每条 RUN 都是一层,删文件必须在同一层里删,否则被删的内容仍然留在上一层。
# 反例:两层,缓存仍在镜像里 RUN apt-get update && apt-get install -y curl RUN apt-get clean # 正例:一层完成 RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/*
.dockerignore 同样重要。我们之前没写,COPY . . 把 target/、.git/、node_modules 全送进了构建上下文,构建慢,还容易误打包本地配置:
.git target node_modules *.log .idea
六、改造前后对比
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 镜像体积 | 1.21 GB | 118 MB |
| 层数 | 23 | 7 |
| 拉取耗时(同机房) | 3 分 20 秒 | 18 秒 |
| 扫描高危漏洞 | 112 | 9 |
| Pod 冷启动 | 26 秒 | 11 秒 |
七、几个容易踩的坑
时区丢失。换了精简基础镜像后容器里是 UTC,日志时间对不上,需要显式装 tzdata 并设 TZ=Asia/Shanghai。
CA 证书缺失。极简镜像可能没有 ca-certificates,调 HTTPS 接口直接报证书校验失败,报错信息还很不直观。
用 latest 标签。基础镜像一变,构建结果跟着变,出问题无法回溯。基础镜像必须锁版本,关键场景连 digest 一起锁。
只优化体积,没优化构建缓存。把 COPY pom.xml 和 COPY src 分开,依赖层才能复用,否则每次改一行代码都要重下全部依赖,构建时间反而变长。
八、落地顺序建议
先上多阶段构建(收益最大、风险最低),再补 .dockerignore,然后合并 RUN 清缓存,最后才考虑换基础镜像。每步都用 docker history 和镜像扫描结果验证一次,别一次改完再统一排查,出了问题很难定位是哪一步引入的。
镜像瘦身本质上是把"构建环境"和"运行环境"彻底分开。想清楚运行时到底需要哪几个文件,剩下的自然就该扔掉。
- 点赞
- 收藏
- 关注作者
评论(0)