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%(攻击面小)
避坑清单
- 用 distroless / scratch,不用 ubuntu/centos base
- 多阶段构建,builder 和 runtime 分开
- .dockerignore 必写,排除 .git/node_modules/target
- 层缓存:依赖单独一层,源码常变层放后面
- BuildKit 缓存挂载:--mount=type=cache
- Java 用 jlink 裁剪 JRE
- Python 不要用 alpine(musl libc 兼容性差)
- Go 静态编译 + scratch / distroless
- CI 必上 cache(kaniko --cache / buildx cache-to)
- 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