Dockerfile 写作精讲 / 15 - 镜像瘦身
15 - 镜像瘦身:层合并、UPX 压缩与符号剥离
15.1 为什么要瘦身
镜像体积直接影响多个运维指标:
| 指标 | 大镜像的影响 | 小镜像的优势 |
|---|
| 拉取时间 | CI/CD 流水线变慢 | 快速部署 |
| 存储成本 | Registry 存储费用增加 | 节省存储 |
| 攻击面 | 包含更多不必要的工具 | 最小化漏洞暴露 |
| 启动时间 | 容器启动延迟 | 秒级启动 |
| 网络带宽 | 大规模部署时带宽消耗大 | 节省带宽 |
15.2 镜像体积分析工具
docker history
# 查看镜像层及每层大小
docker history myapp:latest
# 输出示例:
# IMAGE CREATED SIZE COMMENT
# <missing> 2 hours ago 0B COPY . . # 每层大小
# <missing> 2 hours ago 45MB RUN npm ci --production
# <missing> 3 hours ago 5MB COPY package.json ./
dive
# 安装 dive
# https://github.com/wagoodman/dive
# 交互式分析
dive myapp:latest
# CI 模式
dive myapp:latest --ci
docker scout
docker scout quickview myapp:latest
docker scout cves myapp:latest
15.3 层合并
合并 RUN 指令
# ❌ 多层,每层都保留删除前的内容
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# 最终体积: ~120MB(清理无效,前层仍保留)
# ✅ 合并为一层
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 最终体积: ~80MB
层合并的原则
| 原则 | 说明 |
|---|
| 同一逻辑操作合并 | 安装包 + 清理缓存 = 1 层 |
| 不合并会导致膨胀 | 文件在前层仍占用空间 |
| 不要过度合并 | 影响缓存粒度 |
15.4 基础镜像选择与体积
| 镜像 | 压缩体积 | 解压体积 |
|---|
alpine:3.19 | ~3.5MB | ~7MB |
debian:bookworm-slim | ~25MB | ~75MB |
ubuntu:22.04 | ~28MB | ~77MB |
distroless/static | ~1MB | ~2MB |
distroless/base | ~8MB | ~20MB |
scratch | 0B | 0B |
# ❌ 使用完整 Ubuntu
FROM ubuntu:22.04
# 基础体积: 77MB
# ✅ 使用 slim
FROM debian:bookworm-slim
# 基础体积: 75MB
# ✅✅ 使用 Alpine
FROM alpine:3.19
# 基础体积: 7MB
# ✅✅✅ 使用 Distroless
FROM gcr.io/distroless/static-debian12:nonroot
# 基础体积: 2MB
15.5 二进制压缩:UPX
UPX (Ultimate Packer for eXecutables) 可以压缩可执行文件,通常能减少 50-70% 的体积。
Go 二进制使用 UPX
FROM golang:1.22-alpine AS builder
# 安装 UPX
RUN apk add --no-cache upx
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 编译
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server ./cmd/server
# 使用 UPX 压缩(--best 最高压缩率)
RUN upx --best --lzma /server
# 验证
RUN ls -lh /server
# 压缩前: 15MB
# 压缩后: 5MB
UPX 的注意事项
| 注意点 | 说明 |
|---|
| 启动时间 | UPX 解压会增加约 50-100ms 启动时间 |
| 内存占用 | 解压后内存占用与未压缩相同 |
| 静态链接 | 对静态链接二进制效果最好 |
| 调试 | 压缩后的二进制难以调试 |
| 安全扫描 | 压缩后可能被误判为恶意软件 |
| CGO | 对 CGO 编译的二进制可能不兼容 |
Go 编译优化(不用 UPX)
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-ldflags="-s -w -extldflags '-static'" \
-trimpath \
-o /server \
./cmd/server
# -s: 去除符号表
# -w: 去除 DWARF 调试信息
# -trimpath: 去除编译路径信息
# 效果: 15MB → 8MB
15.6 符号剥离
C/C++ 符号剥离
FROM gcc:13 AS builder
WORKDIR /src
COPY . .
# 编译并剥离符号
RUN gcc -O2 -o app app.c && \
strip --strip-all app
# 对比
RUN ls -lh app
# 剥离前: 1.2MB
# 剥离后: 600KB
Rust 符号剥离
FROM rust:1.77-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /src
COPY . .
# Cargo.toml 中配置:
# [profile.release]
# strip = true
# lto = true
# codegen-units = 1
# opt-level = "z"
RUN cargo build --release
# 或手动剥离
RUN strip target/release/server
# Cargo.toml
[profile.release]
strip = true # 剥离符号
lto = true # 链接时优化
codegen-units = 1 # 单编译单元(更优化但编译慢)
opt-level = "z" # 优化体积而非速度
panic = "abort" # panic 时直接 abort
15.7 依赖优化
Node.js
# ❌ 安装所有依赖(包括 devDependencies)
RUN npm install
# node_modules: 300MB
# ✅ 仅安装生产依赖
RUN npm ci --production
# node_modules: 80MB
# ✅✅ 使用 npm ci + 清理
RUN npm ci --production && npm cache clean --force
# 更小
Python
# ❌ 默认 pip 缓存
RUN pip install -r requirements.txt
# ~/.cache/pip 增加数十 MB
# ✅ 禁用缓存
RUN pip install --no-cache-dir -r requirements.txt
# ✅✅ 使用 --prefix 分离后复制
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 从 builder 阶段仅复制 /install
Java
# ✅ 使用 Spring Boot 分层 JAR
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
15.8 无用文件清理
FROM python:3.12-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y --auto-remove gcc \
&& rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/* \
/root/.cache \
/usr/share/doc \
/usr/share/man \
/usr/share/locale
常见可清理目录
| 目录 | 内容 | 可清理 |
|---|
/var/lib/apt/lists/* | apt 包索引 | ✅ |
/var/cache/apt/* | apt 下载缓存 | ✅ |
/tmp/* | 临时文件 | ✅ |
/root/.cache | pip/npm 缓存 | ✅ |
/usr/share/doc | 文档 | ✅ |
/usr/share/man | man 手册 | ✅ |
/usr/share/locale | 本地化文件 | ✅(保留需要的) |
__pycache__ | Python 字节码缓存 | ✅ |
*.pyc | Python 编译文件 | ✅ |
node_modules/.cache | 构建缓存 | ✅ |
15.9 体积对比实战
Go 应用瘦身全过程
# 阶段一:完整镜像(无优化)
FROM golang:1.22
WORKDIR /src
COPY . .
RUN go build -o /server ./cmd/server
# 最终镜像大小: ~1.2GB
# 阶段二:Alpine 基础镜像
FROM golang:1.22-alpine
WORKDIR /src
COPY . .
RUN go build -o /server ./cmd/server
# 最终镜像大小: ~350MB
# 阶段三:多阶段构建
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o /server ./cmd/server
FROM alpine:3.19
COPY --from=builder /server /server
# 最终镜像大小: ~30MB
# 阶段四:静态编译 + Distroless
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
# 最终镜像大小: ~10MB
# 阶段五:UPX 压缩
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache upx
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server ./cmd/server && \
upx --best --lzma /server
FROM scratch
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 最终镜像大小: ~5MB
| 优化步骤 | 镜像大小 | 缩减比例 |
|---|
| 无优化 | 1.2GB | — |
| Alpine 基础 | 350MB | -71% |
| 多阶段构建 | 30MB | -97% |
| 静态编译 + Distroless | 10MB | -99% |
| UPX 压缩 | 5MB | -99.6% |
15.10 扩展阅读
上一章:14 - 常见构建模式
下一章:16 - 测试与验证 — Hadolint、容器内测试与 CI 集成。