强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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
scratch0B0B
# ❌ 使用完整 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/.cachepip/npm 缓存
/usr/share/doc文档
/usr/share/manman 手册
/usr/share/locale本地化文件✅(保留需要的)
__pycache__Python 字节码缓存
*.pycPython 编译文件
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%
静态编译 + Distroless10MB-99%
UPX 压缩5MB-99.6%

15.10 扩展阅读


上一章14 - 常见构建模式 下一章16 - 测试与验证 — Hadolint、容器内测试与 CI 集成。