跳到主要内容

镜像分层与体积优化

1. 分层原理

每条 RUN / COPY / ADD 生成一层(layer)。镜像 = 多层只读叠加。

docker history myapp:latest
# IMAGE CREATED SIZE COMMENT
# abc123 2 min ago 50MB RUN npm ci
# def456 3 min ago 1.2KB COPY package.json
# 718a9b... 6 days ago 180MB /bin/sh -c #(nop) ADD file:...

docker history 只看自己镜像的层。dive 工具看更详细:

brew install dive
dive myapp:latest
# 交互式查看每层文件变化、浪费空间

2. 基础镜像选择

镜像大小适合
ubuntu:22.0480MB需要完整 apt 生态
debian:slim80MB类似 ubuntu,更小
node:20380MB含完整 build tools
node:20-slim80MBDebian slim 基底
node:20-alpine50MB最小
gcr.io/distroless/nodejs20100MB无 shell、无包管理器,安全
scratch0静态二进制(Go)

2.1 Alpine 注意事项

Alpine 用 musl libc,不是 glibc。某些 native 模块不兼容:

  • node-canvas:要装额外编译依赖
  • node-gyp 编译:可能找不到 glibc 头
  • prisma:需要特定 binaryTargets

如果踩坑严重换 slim(Debian)。

2.2 Distroless

Google 推出,无 shell、无 apt、无 ls。攻击面极小。

FROM node:20 AS builder
# ... build

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app /app
WORKDIR /app
CMD ["server.js"]

无法 docker exec 进去 debug。生产推荐,开发慎用。

3. 减小体积的技巧

3.1 合并 RUN

# ✗ 三层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✓ 一层(rm 在同一 RUN 才有效)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*

rm 必须和 install 在同一 RUN,不然下层文件还在镜像里。

3.2 多阶段抛弃 build cache

模块「Dockerfile 最佳实践」已详述。再举一例:

# Go 二进制极致瘦身
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app .

FROM scratch
COPY --from=builder /app/app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
CMD ["/app"]
# 最终: ~10MB

3.3 Buildkit cache mount

# syntax=docker/dockerfile:1
FROM node:20-alpine
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
npm ci

cache mount 不进镜像但下次构建复用。CI 里加速 5-10 倍。

3.4 删 npm/pip cache

RUN npm ci && npm cache clean --force
RUN pip install -r requirements.txt && rm -rf ~/.cache/pip

3.5 不要用 ADD 解压

ADD foo.tar.gz / 会自动解压,但生成的 layer 包含原文件 + 解压结果,臃肿。手动 wget + tar + rm。

4. squash(已不推荐)

老办法把多层压缩一层,破坏缓存。BuildKit 时代用多阶段构建即可。

5. 看镜像里到底有什么

# 导出镜像
docker save myapp:latest | tar -tv | sort -k3 -n | tail -50

# 用 dive 交互式
dive myapp:latest

# 用 docker scout
docker scout cves myapp:latest # 漏洞扫描
docker scout recommendations myapp:latest # 优化建议

6. 多架构镜像(amd64 + arm64)

Apple Silicon 时代,本地 M 芯片 build 出来是 arm64,部署到 x86 服务器跑不了。

# 一次构建多架构
docker buildx create --name builder --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

镜像仓库会存 manifest list,pull 时按客户端架构自动选。

7. 实测对比

同一 Next.js 应用:

方案镜像大小
全装 node:201.2GB
node:20-slim 多阶段280MB
node:20-alpine 多阶段130MB
node:20-alpine + standalone90MB
distroless + standalone105MB

8. 常见反模式

  • 基础镜像用 latest:构建结果飘移
  • 每条命令一个 RUN:层数膨胀
  • rm 和 install 不在一个 RUN:原文件留下层里
  • ADD 解压:layer 含双份
  • 不用多阶段:build tools 进生产镜像
  • 本地 build arm64 推服务器:架构不匹配挂掉
  • 不删 cache:每个 RUN 留几十到几百 MB
  • 复制整个 .git:build context 巨大、可能泄密

9. 延伸阅读