Docker 镜像体积半年涨到 2GB?一套分层瘦身与构建缓存优化实战
问题现象
某 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% 的体积。
- 点赞
- 收藏
- 关注作者
评论(0)