Docker 镜像 1.8GB 瘦身到 180MB:多阶段构建 + 层缓存实战

80 微服务 CI/CD,Java 镜像 1.8GB 构建 8min,K8s 拉取慢,节点磁盘告警。两周治理:多阶段构建 + jlink 裁剪 JRE + distroless/scratch + 层缓存指令排序 + BuildKit cache mount + Trivy 安全扫描。镜像 180MB,构建 90s,流水线提速 60%。

2024 年我们的 CI/CD 流水线:80 个微服务,每天构建 600+ 次,某次梳理发现镜像构建成了瓶颈 — 单个 Java 服务镜像 1.8GB,构建 8 分钟,推送 3 分钟,K8s 拉取启动慢,节点磁盘频繁告警。投了两周做镜像治理,镜像体积 1.8GB → 180MB,构建 8min → 90s,整条流水线提速 60%。本文复盘 Docker 镜像构建优化的完整实战,覆盖多阶段构建、层缓存、基础镜像、体积瘦身、安全扫描。

问题背景

CI/CD:GitLab CI + Kaniko 构建
服务:80 个微服务(Java 50 / Go 20 / Node 10)
构建频率:600+ 次/天

问题:
- Java 服务镜像:1.8GB(openjdk 全量 + 依赖 + 源码)
- Go 服务镜像:900MB(golang 全镜像直接跑)
- Node 服务镜像:1.2GB(node_modules + 源码)
- 构建时长:Java 8min / Go 5min / Node 6min
- 推送 registry:2-3min
- K8s 节点磁盘:镜像层占 80%+,频繁清理
- Pod 拉镜像启动:首次 90s+

镜像层分析(dive 工具):
$ dive myapp:latest
Total Image size: 1.8 GB
Potential wasted space: 680 MB     ← 浪费近 700MB
  - 多次 COPY 同样文件
  - apt cache 没清理
  - 构建工具留在最终镜像
Image efficiency score: 62%

修复 1:多阶段构建(Java)

# 不好:单阶段,构建工具 + 源码全留在镜像
# FROM maven:3.9-openjdk-17
# COPY . .
# RUN mvn package
# CMD ["java", "-jar", "target/app.jar"]
# → 1.8GB(含 maven / JDK / .m2 / 源码)

# 好:多阶段构建
# === 阶段 1:构建 ===
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build

# 先拷 pom.xml 单独下依赖(利用层缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 再拷源码构建(源码变了不会重下依赖)
COPY src ./src
RUN mvn package -DskipTests -B

# === 阶段 2:运行 ===
FROM eclipse-temurin:17-jre-jammy AS runtime
WORKDIR /app

# 只拷构建产物
COPY --from=builder /build/target/app.jar app.jar

# 非 root 用户运行(安全)
RUN groupadd -r app && useradd -r -g app app
USER app

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

# 结果:JRE 而非 JDK,无 maven,无源码 → 350MB

# === 进阶:用 jlink 裁剪 JRE ===
FROM eclipse-temurin:17-jdk-jammy AS jre-builder
RUN jlink \
    --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.security.jgss,java.instrument,jdk.unsupported \
    --strip-debug --no-man-pages --no-header-files \
    --compress=2 --output /custom-jre

FROM debian:12-slim AS runtime
COPY --from=jre-builder /custom-jre /opt/jre
ENV PATH="/opt/jre/bin:$PATH"
COPY --from=builder /build/target/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# 裁剪 JRE → 最终镜像 180MB

修复 2:多阶段构建(Go)

# Go 镜像优化:静态编译 + scratch/distroless

# === 阶段 1:构建 ===
FROM golang:1.22-alpine AS builder
WORKDIR /build

# 先拷 go.mod 下依赖(层缓存)
COPY go.mod go.sum ./
RUN go mod download

# 拷源码编译
COPY . .
# CGO_ENABLED=0 静态编译,-ldflags 去符号表
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -extldflags '-static'" \
    -trimpath \
    -o /app ./cmd/server

# === 阶段 2:最小运行镜像 ===
# 方案 A:scratch(空镜像,最小)
FROM scratch
# 需要手动拷 ca 证书 + 时区
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
# → 12MB

# 方案 B:distroless(谷歌官方,带基础但无 shell)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
# → 14MB,比 scratch 多了 ca/tz/nonroot 用户,推荐

# 对比:
# golang:1.22 全镜像跑     → 900MB
# alpine + 二进制          → 25MB
# distroless               → 14MB
# scratch                  → 12MB

修复 3:Node.js 镜像优化

# === 阶段 1:依赖安装 ===
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# 只装生产依赖
RUN npm ci --omit=dev && npm cache clean --force

# === 阶段 2:构建 ===
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci                       # 含 devDependencies
COPY . .
RUN npm run build                # 产出 dist/

# === 阶段 3:运行 ===
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

# 只拷生产依赖 + 构建产物
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

# 非 root
USER node

EXPOSE 3000
CMD ["node", "dist/main.js"]

# .dockerignore(关键,减少构建上下文)
# node_modules
# dist
# .git
# *.log
# coverage
# .env*
# README.md
# tests

# 结果:1.2GB → 220MB
# 进一步:用 distroless/nodejs → 180MB

修复 4:层缓存优化

# 原则:变化频率低的指令放前面,高的放后面
# Docker 层缓存:某层变了,后面所有层缓存失效

# 不好:源码一变,依赖重装
FROM node:20-alpine
WORKDIR /app
COPY . .                          # 源码 + package.json 一起拷
RUN npm ci                        # 任何文件改动都重装依赖

# 好:依赖和源码分开
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./   # 只拷依赖描述
RUN npm ci                                # 依赖没变就命中缓存
COPY . .                                  # 源码单独拷
RUN npm run build

# === RUN 指令合并(减少层数 + 清理缓存)===
# 不好:apt cache 留在镜像里
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# 好:一条 RUN,装完即清理
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# === BuildKit 缓存挂载(依赖缓存跨构建复用)===
# syntax=docker/dockerfile:1.4
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
# --mount=type=cache:.m2 缓存持久化,不进镜像层
RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
    mvn package -DskipTests -B

# Go 同理
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /app ./cmd/server

# CI 中启用 BuildKit
# DOCKER_BUILDKIT=1 docker build ...
# 或 buildx:docker buildx build --cache-from --cache-to

修复 5:CI 构建加速

# GitLab CI:用 buildx + registry 缓存
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_BUILDKIT: "1"
  script:
    - docker buildx create --use
    # --cache-from / --cache-to 用 registry 做缓存层
    - docker buildx build
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        --push .

# Kaniko 构建(K8s 内无 docker daemon)
build-kaniko:
  image:
    name: gcr.io/kaniko-project/executor:debug
  script:
    - /kaniko/executor
        --context $CI_PROJECT_DIR
        --dockerfile $CI_PROJECT_DIR/Dockerfile
        --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        --cache=true
        --cache-repo=$CI_REGISTRY_IMAGE/cache
        --snapshot-mode=redo
        --use-new-run

# 构建矩阵并行(80 服务分批)
# 用 GitLab parallel:matrix 或 needs DAG

# 镜像分层标签策略
# - :latest        最新
# - :$CI_COMMIT_SHA 精确版本(部署用这个)
# - :$BRANCH        分支最新
# - :buildcache     缓存专用

# 效果:
# - 全 cache 命中:90s 完成
# - 依赖变更:3min
# - 完全冷构建:5min

修复 6:镜像安全扫描

# 1. Trivy 扫描(CVE 漏洞 + 配置)
$ trivy image --severity HIGH,CRITICAL myapp:latest

# 输出
myapp:latest (debian 12.4)
Total: 3 (HIGH: 2, CRITICAL: 1)
┌──────────────┬────────────────┬──────────┬───────────────────┐
│   Library    │ Vulnerability  │ Severity │  Fixed Version    │
├──────────────┼────────────────┼──────────┼───────────────────┤
│ libssl3      │ CVE-2024-xxxx  │ CRITICAL │ 3.0.13-1~deb12u1  │
└──────────────┴────────────────┴──────────┴───────────────────┘

# 2. CI 中集成(发现 CRITICAL 就 fail)
scan:
  stage: test
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity CRITICAL
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  allow_failure: false

# 3. 减少攻击面的措施
# - 用 distroless / slim 基础镜像(无 shell / 无包管理器)
# - 非 root 用户运行
# - 只装必要的包(--no-install-recommends)
# - 定期更新基础镜像
# - 不在镜像里放密钥(用 K8s Secret)

# 4. 镜像签名(供应链安全)
$ cosign sign --key cosign.key myapp:latest
$ cosign verify --key cosign.pub myapp:latest

# 5. dive 分析镜像效率
$ dive myapp:latest --ci --highestUserWastedPercent 0.1
# 浪费空间超阈值则 CI 失败

# 6. SBOM 生成(软件物料清单)
$ trivy image --format cyclonedx --output sbom.json myapp:latest
$ syft myapp:latest -o spdx-json

优化效果

指标                  优化前         优化后
=========================================================
Java 镜像体积        1.8GB          180MB(jlink JRE)
Go 镜像体积          900MB          14MB(distroless)
Node 镜像体积        1.2GB          180MB
构建时长(冷)        8min           5min
构建时长(缓存命中)  8min           90s
推送 registry        2-3min         10-20s
K8s 首次拉取启动     90s            12s
镜像效率分(dive)    62%            96%
节点磁盘镜像占用      80%+           25%

整体收益:
- CI 流水线:平均 25min → 10min
- registry 存储:8TB → 1.5TB
- K8s 节点磁盘告警:消失
- 部署速度快 5x,回滚也快

安全:
- CVE 扫描入 CI,CRITICAL 阻断发布
- distroless 无 shell,攻击面大幅缩小
- 镜像签名 + SBOM,供应链可追溯

避坑清单

  1. 多阶段构建:builder 编译,runtime 只拷产物
  2. 基础镜像选最小:JRE 而非 JDK,distroless/scratch 而非全镜像
  3. 层缓存:依赖描述文件(pom/go.mod/package.json)单独 COPY 在前
  4. RUN 指令合并 + 清理缓存(apt clean / rm -rf lists)
  5. BuildKit cache mount:依赖缓存不进镜像层
  6. .dockerignore 必写,减小构建上下文
  7. 非 root 用户运行,安全基线
  8. CI 用 registry 缓存(--cache-from/--cache-to)
  9. Trivy 扫描入 CI,CRITICAL 漏洞阻断发布
  10. 用 dive 分析镜像效率,浪费空间纳入 CI 检查

总结

Docker 镜像优化是个性价比极高的工程,几行 Dockerfile 改动就能换来 10 倍体积缩减和构建提速。最大的认知改变:镜像里不该有构建工具,maven / JDK / node_modules 的 devDependencies / Go 编译器,这些只在构建阶段需要,多阶段构建让 runtime 镜像只保留运行所需的最小集合 —— 一个 Go 服务的最终镜像可以只有 14MB。最被低估的是层缓存的指令顺序,把 COPY package.json 和 RUN npm ci 放在 COPY 源码之前,源码每次改动都不会触发依赖重装,这一个顺序调整就能让 90% 的构建走缓存、90 秒完成。最容易踩的坑是 RUN 指令不清理缓存,apt-get install 后不 clean,几百 MB 的包索引就永久留在镜像层里,而且因为分层机制,后面的 RUN 删除也救不回来 —— 必须在同一条 RUN 里装完即清。最后,distroless 不只是为了瘦身,它没有 shell、没有包管理器,攻击者即使进了容器也无从下手,体积和安全一举两得,是 2024 年生产镜像的首选基础。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

PostgreSQL 6 亿行大表慢查询复盘:索引 + 分区 + 参数调优实战

2026-5-20 10:47:54

技术教程

MySQL 12 亿行单表分库分表实战:ShardingSphere + 基因法 + 不停机迁移

2026-5-20 10:51:54

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索