Docker 镜像瘦身实录:80 服务从 800MB 平均降到 120MB

80 个微服务镜像平均 800MB,CI 25min。三周瘦身实录覆盖 Java/Node/Python/Go 四个栈:distroless + scratch + 多阶段 + BuildKit 缓存挂载 + jlink + Spring AOT + dive/Trivy 扫描。平均降到 120MB,CI 25min→6min,扩容 60s→8s。

2024 年我们的微服务集群有 80 个服务,镜像平均 800MB,CI 一次构建 + 推送要 25 分钟,pull 镜像也慢。投了三周搞镜像瘦身 + 构建加速,平均镜像降到 120MB,CI 时长从 25min 降到 6min,镜像 pull 从 60s 降到 8s。本文复盘 Docker 镜像优化的 15 个手段,覆盖 Java / Node / Python / Go 四个主要技术栈。

问题背景

微服务集群:80 个服务
镜像仓库:Harbor(私有)
CI:GitLab CI + Kaniko
日构建:300 次/天

性能问题:
- Java 镜像平均 800MB(openjdk:17 base)
- Python 镜像平均 1.2GB(数据栈)
- Node 镜像平均 600MB(全 node_modules)
- Go 镜像平均 50MB(已经小,但能更小)
- CI 一次构建 25 min(80% 时间在拉/推)
- 镜像 pull 慢:大促扩容 60s 起步

需要全面优化

通用原则

1. 用更小的 base 镜像(scratch / distroless / alpine)
2. 多阶段构建(builder vs runtime 分开)
3. 层缓存利用(频繁变的放后面)
4. 不装无用东西(--no-install-recommends, --no-cache)
5. 一行 RUN 合并命令(减少层数,但不要过度)
6. .dockerignore 不打无关文件
7. 用 BuildKit 缓存挂载

Java 镜像优化(800MB → 90MB)

# 不好(800MB)
FROM openjdk:17
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

# 第一步:JRE only(350MB)
FROM eclipse-temurin:17-jre-alpine
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

# 第二步:jlink 自定义 JRE(150MB)
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /build
COPY pom.xml .
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
RUN jdeps --multi-release 17 --print-module-deps \
          --ignore-missing-deps target/*.jar > deps.txt

FROM eclipse-temurin:17-jdk AS jlink
COPY --from=builder /build/deps.txt /tmp/
RUN jlink \
    --add-modules $(cat /tmp/deps.txt),java.security.jgss,jdk.crypto.ec,jdk.unsupported \
    --strip-debug --no-man-pages --no-header-files --compress=2 \
    --output /opt/jre

# 第三步:distroless runtime(90MB)
FROM gcr.io/distroless/java-base-debian12:nonroot
COPY --from=jlink /opt/jre /opt/jre
COPY --from=builder /build/target/*.jar /app/app.jar
WORKDIR /app
USER nonroot
ENV PATH="/opt/jre/bin:$PATH"
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

# Spring AOT 还能更小(60MB)
FROM eclipse-temurin:17-jdk AS native
RUN --mount=type=cache,target=/root/.m2 \
    mvn -Pnative native:compile
FROM gcr.io/distroless/cc-debian12
COPY --from=native /build/target/app /app
ENTRYPOINT ["/app"]

Node.js 镜像优化(600MB → 80MB)

# 不好(600MB)
FROM node:20
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "server.js"]

# 第一步:node-alpine(180MB)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]

# 第二步:多阶段 + standalone(120MB)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nodejs
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]

# 第三步:distroless(80MB)
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/.next/standalone /app
WORKDIR /app
USER 1001
CMD ["server.js"]

Python 镜像优化(1.2GB → 200MB)

# 不好(1.2GB)
FROM python:3.11
RUN pip install -r requirements.txt
COPY . /app
WORKDIR /app
CMD ["python", "app.py"]

# 第一步:slim(450MB)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

# 第二步:多阶段 + virtualenv(280MB)
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
CMD ["python", "app.py"]

# 第三步:distroless(200MB)
FROM python:3.11-slim AS builder
# ... 上面那段 ...

FROM gcr.io/distroless/python3-debian12
COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
COPY . .
ENV PYTHONPATH=/opt/venv/lib/python3.11/site-packages
CMD ["app.py"]

# 警告:numpy/pandas/torch 这类需要 glibc,不能用 alpine
# Alpine 用 musl libc,大量 wheel 失效,要源码编译,镜像反而大

Go 镜像优化(50MB → 12MB)

# 已经小,但能更小
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    CGO_ENABLED=0 GOOS=linux \
    go build -ldflags='-w -s -extldflags "-static"' \
    -trimpath \
    -o /app .

# scratch(只有二进制)
FROM scratch
COPY --from=builder /app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]

# 12MB(从 50MB 降下来)

# 或 distroless(支持 PID 1 信号)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
USER nonroot
ENTRYPOINT ["/app"]

BuildKit 缓存挂载

# 启用 BuildKit
# syntax=docker/dockerfile:1.6

# 缓存 maven 下载
RUN --mount=type=cache,target=/root/.m2 \
    mvn package -DskipTests

# 缓存 npm
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# 缓存 pip
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# 缓存 go modules
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o app .

# 缓存 apt(系统包)
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y curl wget

# bind mount:不复制源码进镜像,直接挂载
RUN --mount=type=bind,source=.,target=/src,rw \
    cd /src && make build

层缓存优化

# 不好:每次源码改 → 重新装依赖
COPY . /app
WORKDIR /app
RUN npm ci
RUN npm run build

# 好:依赖单独一层
COPY package*.json /app/
WORKDIR /app
RUN npm ci                # 这一层只在 package.json 改才重做
COPY . /app/              # 源码改不影响上层
RUN npm run build

# Java 也一样
COPY pom.xml /build/
RUN mvn dependency:go-offline    # 缓存
COPY src /build/src
RUN mvn package

# Python 也一样
COPY requirements.txt /app/
RUN pip install -r requirements.txt   # 缓存
COPY . /app/

# 黄金法则:不常变的放上面,常变的放下面

.dockerignore 必备

# .dockerignore(以 Java 为例)
.git/
.gitignore
.github/
.idea/
.vscode/
*.md
target/
!target/*.jar      # 只要 jar
*.log
node_modules/
__pycache__/
*.pyc
.pytest_cache/
.env
.env.*
docker-compose*.yml
Dockerfile*
README*

# 效果:
# - 减少 docker build context 大小(原来 500MB → 20MB)
# - 防止误打入敏感文件
# - 提速构建

CI 加速

# GitLab CI 用 Kaniko + cache
build:
  image: gcr.io/kaniko-project/executor:v1.20.0-debug
  variables:
    DOCKER_BUILDKIT: "1"
  script:
    - /kaniko/executor
      --context $CI_PROJECT_DIR
      --dockerfile Dockerfile
      --destination $REGISTRY/$IMAGE:$CI_COMMIT_SHORT_SHA
      --destination $REGISTRY/$IMAGE:latest
      --cache=true
      --cache-repo=$REGISTRY/cache
      --cache-ttl=168h
      --compressed-caching=false
      --snapshot-mode=redo
      --use-new-run

# 或 buildx + GitHub Actions
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ${{ env.IMAGE }}
    cache-from: type=registry,ref=${{ env.IMAGE }}:cache
    cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=max
    platforms: linux/amd64,linux/arm64

# 效果:
# 首次构建 8min,后续构建 1-2min(80% 缓存命中)

镜像安全 + 体积扫描

# Trivy 扫漏洞
$ trivy image myimage:latest
- HIGH: 5
- CRITICAL: 1

# dive 分析镜像层(看哪些层占空间)
$ dive myimage:latest
Layer 1: 20MB  base
Layer 2: 60MB  deps
Layer 3: 5MB   code
Total: 85MB
Efficiency: 96%

# docker-slim 自动瘦身(实验性)
$ docker-slim build --target myimage:latest --tag myimage:slim
# 自动分析运行时实际用到的文件,删掉没用的

镜像 Pull 加速

# 1. Harbor + 镜像分发(DragonFly / Kraken)
# K8s 节点间 P2P 分发,大集群 pull 时间减少 80%

# 2. 镜像预热
# 在节点添加时预拉常用镜像
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
imageMinimumGCAge: 2m
imageGCHighThresholdPercent: 85
imageGCLowThresholdPercent: 80

# 3. K8s imagePullPolicy
imagePullPolicy: IfNotPresent       # 默认即可
# Always:每次 pull(慢)
# Never:本地必须有(适合 CI 镜像)

# 4. nydus / estargz(惰性 pull)
# 镜像启动时只 pull 必要文件,运行时按需 pull
# 启动时间从 60s 降到 5s

优化效果

技术栈      优化前          优化后        变化
=====================================================
Java        800MB           90MB          -89%
Node.js     600MB           80MB          -87%
Python      1.2GB           200MB         -83%
Go          50MB            12MB          -76%
平均        800MB           120MB         -85%

CI 时长(构建+推送)
单服务      8min            1.5min        -81%
全集群批量  25min           6min          -76%

Pull 时间
单服务      60s             8s            -87%
扩容 100 Pod  3min          15s            -92%

业务影响:
- 大促弹性扩容速度 +10x
- CI 资源占用 -60%
- 镜像仓库存储 -85%
- 安全漏洞数 -70%(攻击面小)

避坑清单

  1. 用 distroless / scratch,不用 ubuntu/centos base
  2. 多阶段构建,builder 和 runtime 分开
  3. .dockerignore 必写,排除 .git/node_modules/target
  4. 层缓存:依赖单独一层,源码常变层放后面
  5. BuildKit 缓存挂载:--mount=type=cache
  6. Java 用 jlink 裁剪 JRE
  7. Python 不要用 alpine(musl libc 兼容性差)
  8. Go 静态编译 + scratch / distroless
  9. CI 必上 cache(kaniko --cache / buildx cache-to)
  10. Trivy 漏洞扫描 + dive 体积分析 + docker-slim 自动瘦身

总结

Docker 镜像优化是 K8s 集群的隐藏收益:镜像小 = 部署快 + 攻击面小 + 存储省。这次 80 个微服务全面优化,平均镜像 800MB → 120MB,部署速度提升 10x。最大的认知改变:镜像优化不是"能小就小",而是"该用 distroless 用 distroless,该用 jlink 用 jlink"。Alpine 不是银弹(musl libc 兼容性差),scratch 也不是银弹(不能 exec)。每个技术栈都有适合的 base 镜像和最佳实践。最后,生产用 distroless + nonroot user 不仅小,还更安全 — 没 shell、没包管理、攻击面极小。如果你的 CI 还在用 docker build,2024 年值得花一周时间迁移到 BuildKit + 多阶段 + 缓存挂载,收益巨大。

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

Elasticsearch 80 亿日志治理:P99 8s→200ms 磁盘 -60%

2026-5-19 12:53:19

技术教程

gRPC 内部 RPC 切换实录:P99 80ms→18ms 带宽 -71%

2026-5-19 12:57:33

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