Docker 镜像体积半年涨到 2GB?一套分层瘦身与构建缓存优化实战

举报
yd_243555368 发表于 2026/11/04 03:34:58 2026/11/04
【摘要】 镜像每发一版胖一点,节点磁盘被吃满、Pod 拉镜像 4 分钟。本文用 docker history 与 dive 定位胖层,给出多阶段构建拆掉工具链、.dockerignore 掐构建上下文、同层清理、BuildKit 外置 registry 缓存(mode=max)以及体积门禁防回归的完整命令,实测 2.13GB 降到 287MB。

问题现象

某 Java 服务的镜像从半年前的 380MB 涨到了 2.1GB,节点磁盘被镜像吃满,滚动发布时每个 Pod 拉镜像要 4 分多钟,HPA 扩容直接超时。登录节点看:

docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' | sort -k2 -h -r | head -5
# order-svc:v1.9.3   2.13GB
# order-svc:v1.8.0   1.42GB
# order-svc:v1.6.2    386MB

同一个服务、同一套代码,镜像体积每发一版涨一点,这是典型的分层叠加 + 构建缓存失效叠加问题,不是「依赖变多」那么简单。

第一步:先定位是哪一层胖

不要靠猜。用 docker history 看每层实际占用(注意 CREATED BY 被截断,加 --no-trunc 才好看清是哪条指令):

docker history --no-trunc --human order-svc:v1.9.3 \
  | awk 'NR>1 {print $1, $2, substr($0, index($0,$5))}' \
  | sort -k2 -h -r | head -10

常见输出会指向三处:

  • 一层 COPY . . 或 ADD 带进来几百 MB,其实是构建产物、.git、target、node_modules;
  • 一层 RUN apt-get install 没清缓存,/var/lib/apt/lists 单独占几十 MB;
  • 多了一层「装 JDK 又装 Maven 又装 git」,只是构建期需要,运行期一个都用不上。

更细的逐文件分析用 dive(CI 里也能跑):

dive order-svc:v1.9.3 --ci --lowestEfficiency=0.9

第二步:多阶段构建,把构建期依赖全扔掉

胖的主要原因通常是把构建工具链带进了运行镜像。改成多阶段,运行镜像只留 JRE 和 jar:

# syntax=docker/dockerfile:1.7
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B -q dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B -q package -DskipTests

FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
RUN groupadd -r app && useradd -r -g app app
COPY --from=build --chown=app:app /src/target/app.jar ./app.jar
USER app
ENTRYPOINT ["java","-XX:MaxRAMPercentage=75","-jar","/app/app.jar"]

关键点有三个:

  • pom.xml 先 COPY 再 COPY src,源码改动不会让依赖层缓存失效;
  • --mount=type=cache 让 .m2 落宿主机缓存目录,不进镜像层(老写法 RUN mvn ... && rm -rf /root/.m2 是无效的,删除只是新加一层白障,体积不降);
  • 基础镜像换 jre 而不是 jdk,通常直接省 200MB 以上。

第三步:.dockerignore 是最便宜的一招

COPY . . 会把 .git、测试报告、构建产物一起塞进构建上下文,既慢又脏。项目根目录必须有:

cat > .dockerignore <<'EOF'
.git
.gitignore
target
node_modules
dist
*.log
.idea
.vscode
**/*.test
EOF

第四步:装完就清,且在同一条 RUN 里

删除必须在产生文件的同一个 RUN 内完成,跨层删除无效:

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*

Alpine 场景同理:apk add --no-cache 而不是 apk add 后再 apk cache clean。

第五步:让构建缓存真正命中

如果 CI 每次都是 --no-cache 从零构建,上面的层缓存全白搭。BuildKit 可以外置缓存:

docker buildx build \
  --cache-from type=registry,ref=registry.local/order-svc:buildcache \
  --cache-to   type=registry,ref=registry.local/order-svc:buildcache,mode=max \
  --target runtime -t registry.local/order-svc:v1.9.4 --push .

mode=max 才会缓存中间阶段(多阶段构建时,min 只缓存最终阶段,浪费极大)。本地排查可先用 --progress=plain 看 CACHED 标记,哪一步频繁 miss 就固定那一步的输入(版本号、时间戳、构建参数)。

第六步:验证瘦身效果并防回归

docker build -t order-svc:v1.9.4 .
docker images order-svc:v1.9.4 --format '{{.Size}}'   # 期望 2.13GB → 300MB 以内
docker run --rm order-svc:v1.9.4 id                 # 确认非 root
docker run --rm order-svc:v1.9.4 ls -l /app/app.jar  # 文件属主是 app

然后把它写进流水线当门禁,体积超阈值直接失败,防止半年后再次悄悄膨胀:

SIZE=$(docker image inspect order-svc:$TAG --format '{{.Size}}')
LIMIT=$((400*1024*1024))
[ "$SIZE" -le "$LIMIT" ] || { echo "镜像超限: $SIZE > $LIMIT"; exit 1; }

本例改完后:2.13GB → 287MB,Pod 拉镜像从 4 分 12 秒降到 11 秒,HPA 扩容恢复正常。经验顺序是:先 docker history 定位 → 多阶段拆工具链 → .dockerignore 掐上下文 → 同层清理 → 外置缓存 → 体积门禁防回归,六步里前四步通常就能拿回 80% 的体积。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。