Docker 镜像瘦身:多阶段构建与缓存命中技巧
优秀的 Dockerfile 同时追求构建快、镜像小、依赖可审计和结果可复现。最常见的问题是先复制整个仓库再下载依赖,导致任何源码变化都让依赖层缓存失效;或者把编译工具、测试缓存和密钥一起留在运行镜像。
先固定依赖层
Go 项目先复制模块文件:
# syntax=docker/dockerfile:1
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/app ./cmd/app
FROM alpine:3.22
RUN addgroup -S app && adduser -S -G app app
COPY --from=build /out/app /app/app
USER app
ENTRYPOINT ["/app/app"]
前端同理,先复制 package.json 和 Lockfile 安装依赖,再复制源码。Lockfile 必须进入版本控制,CI 使用 npm ci 等确定性命令。
.dockerignore 决定上下文质量
至少排除:
.git
node_modules
dist
coverage
*.log
.env*
_backups
构建上下文越小,传输、Hash 和意外泄密风险越低。不要依赖“Dockerfile 没 COPY 这个文件”来保护密钥,先让它根本不进入上下文。
使用 BuildKit 缓存挂载
缓存挂载能复用包下载与编译缓存,但不会把缓存内容写入最终层。CI 可使用外部缓存:
docker buildx build \
--cache-from type=registry,ref=registry.example.com/app:buildcache \
--cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max \
-t registry.example.com/app:sha-123 .
缓存不是可信输入。依赖校验仍由 Lockfile、Checksum 和软件源完成,不能因为命中缓存就跳过完整性检查。
控制最终镜像
- 编译工具只留在 Builder Stage。
- 运行进程使用非 Root 用户。
- 显式复制时区、CA 证书等真正需要的运行文件。
- 不在镜像中写配置密钥,运行时挂载或使用 Secret。
- 固定基础镜像版本,关键生产环境可固定 Digest。
- 生成 SBOM 并扫描已知漏洞。
Scratch 镜像最小,但诊断能力和证书、时区处理更严格;Distroless 或 Alpine 也各有兼容性取舍。最小不是唯一目标,可维护和可排障同样重要。
找出镜像为什么大
docker history --no-trunc your-image:tag
docker image inspect your-image:tag
删除文件必须与创建文件发生在同一层,否则旧层仍占空间。不要 RUN download 后在下一层 RUN rm。更好的方式是多阶段构建或在同一 RUN 完成并清理。

评论
0 条讨论