镜像分层与体积优化
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.04 | 80MB | 需要完整 apt 生态 |
debian:slim | 80MB | 类似 ubuntu,更小 |
node:20 | 380MB | 含完整 build tools |
node:20-slim | 80MB | Debian slim 基底 |
node:20-alpine | 50MB | 最小 |
gcr.io/distroless/nodejs20 | 100MB | 无 shell、无包管理器,安全 |
scratch | 0 | 静态二进制(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:20 | 1.2GB |
| node:20-slim 多阶段 | 280MB |
| node:20-alpine 多阶段 | 130MB |
| node:20-alpine + standalone | 90MB |
| distroless + standalone | 105MB |
8. 常见反模式
- 基础镜像用 latest:构建结果飘移
- 每条命令一个 RUN:层数膨胀
- rm 和 install 不在一个 RUN:原文件留下层里
ADD解压:layer 含双份- 不用多阶段:build tools 进生产镜像
- 本地 build arm64 推服务器:架构不匹配挂掉
- 不删 cache:每个 RUN 留几十到几百 MB
- 复制整个 .git:build context 巨大、可能泄密