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 集成。