Language:Chinese VersionEnglish Version

你的 Node.js 镜像可能有 1.2 GB。它应该是 120 MB。

我最近审核了一个在 Kubernetes 上运行 15 个微服务的客户的 Docker 镜像。他们的平均镜像大小是 1.4 GB。应用多阶段构建和其他一些优化后,平均大小降至 89 MB——减少了 94%。他们的部署时间从 4 分钟缩短到 45 秒。他们的容器注册表账单减少了 70%。新节点上的冷启动从 30 秒减少到不到 5 秒。

大型 Docker 镜像不仅是存储问题。它们是部署速度问题、安全表面问题和成本问题。你发送的每一层都包含扫描器会标记且你的安全团队会询问的潜在 CVE。每次推送的每兆字节都会在部署期间占用带宽,在扩展事件期间占用拉取带宽。累积影响是巨大的,而解决方案几乎总是很简单。

镜像为什么会变得臃肿

大多数过大的 Docker 镜像源于以下四个原因之一:

1. 使用了错误的基础镜像

最常见的错误是在不需要完整操作系统镜像时从它开始:

# 不好的:900+ MB 基础镜像
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nodejs npm
COPY . /app
RUN npm install
CMD ["node", "server.js"]
# 最终镜像:~1.2 GB

# 更好:340 MB 基础镜像  
FROM node:20
COPY . /app
RUN npm install
CMD ["node", "server.js"]
# 最终镜像:~400 MB

# 最佳:50 MB 基础镜像
FROM node:20-alpine
COPY . /app
RUN npm install --production
CMD ["node", "server.js"]
# 最终镜像:~80 MB

基于 Alpine 的镜像使用 musl libc 而不是 glibc,这偶尔会导致与原生 Node.js 插件(bcrypt、sharp 等)的兼容性问题。对于这些情况,请使用 slim 变体:

FROM node:20-slim
# ~200 MB 基础镜像,glibc 兼容,仍然比完整的 node:20 小得多

2. 在运行时镜像中包含构建依赖

这是多阶段构建直接解决的问题。当你编译 Go 二进制文件时,需要 Go 工具链。当你构建 React 应用时,需要 Node.js、npm 和 webpack。当你编译 Python C 扩展时,需要 gcc 和 python-dev。这些都不应该出现在你的生产镜像中。

3. 没有利用层缓存

Docker 将每个指令构建为一个层,并且层会被缓存。如果你在安装依赖之前复制整个源代码树,那么更改一行代码就会使依赖安装缓存失效:

# 不好的:任何代码更改都会重新构建 node_modules
COPY . /app
RUN npm install

# 好:依赖项会被缓存,除非 package.json 发生变化
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app

4. 忘记了 .dockerignore

如果没有 .dockerignore 文件,Docker 会将构建上下文中的所有内容发送到守护进程,包括 node_modules、.git、测试文件、文档和 IDE 配置:

# .dockerignore
node_modules
.git
.gitignore
*.md
docs/
tests/
.env*
.vscode/
.idea/
coverage/
dist/
Dockerfile
docker-compose*.yml

多阶段构建:完整指南

多阶段构建允许您在单个 Dockerfile 中使用多个 FROM 语句。每个 FROM 都会启动一个新阶段,您可以将工件从一个阶段复制到另一个阶段。最终镜像只包含最后阶段的层。

模式1:Go 应用程序

Go 是多阶段构建的典型代表,因为 Go 会编译成不需要运行时的静态二进制文件:

# 阶段1:构建
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server

# 阶段2:运行时
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

# 构建镜像:~800 MB(Go 工具链 + 依赖项)
# 最终镜像:~8 MB(仅二进制文件 + CA 证书)

-ldflags="-w -s" 标志会剥离调试信息,将二进制文件大小减少 20-30%。使用 CGO_ENABLED=0 构建会产生一个完全静态的二进制文件,可以在 scratch(一个空的基镜像)上运行。

模式2:Node.js 应用程序

# 阶段1:安装依赖
FROM node:20-alpine AS deps

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production

# 阶段2:构建(如果您有构建步骤)
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# 阶段3:运行时
FROM node:20-alpine

RUN addgroup -g 1001 -S nodejs && 
    adduser -S nextjs -u 1001

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

USER nextjs
EXPOSE 3000
CMD ["node", "dist/server.js"]

# 最终镜像:~120 MB(Alpine + Node 运行时 + 生产依赖 + 构建代码)

模式3:Python 应用程序

# 阶段1:构建 wheel 文件
FROM python:3.12-slim AS builder

WORKDIR /app
RUN pip install --no-cache-dir build wheel

COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# 阶段2:运行时
FROM python:3.12-slim

RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* && 
    rm -rf /wheels

COPY . .
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000", "-w", "4"]

# 最终镜像:~180 MB(相比完整的 python:3.12 + 构建工具的 ~900 MB)

模式 4: Rust 应用程序

# 阶段 1: 构建
FROM rust:1.77-slim AS builder

WORKDIR /app
# 缓存依赖编译
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && 
    cargo build --release && 
    rm -rf src

COPY src ./src
RUN cargo build --release

# 阶段 2: 运行时
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends 
    ca-certificates && 
    rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/myapp /usr/local/bin/

EXPOSE 8080
CMD ["myapp"]

# 最终镜像: ~80 MB (精简 Debian + 静态二进制文件 + CA 证书)

高级优化技术

使用 Distroless 镜像

Google 的 distroless 镜像只包含您的应用程序及其运行时依赖项 — 没有 shell,没有包管理器,没有实用工具。这大大减少了攻击面:

# 对于 Java 应用程序
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar

FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/build/libs/app.jar /app.jar
CMD ["app.jar"]
# 无 shell 访问 = 无 shell 漏洞利用

Buildkit 缓存挂载

Docker BuildKit 支持缓存挂载,这些缓存在构建之间持久存在,但不包含在最终镜像中:

# syntax=docker/dockerfile:1

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./

# 跨构建缓存 Go 模块
RUN --mount=type=cache,target=/go/pkg/mod 
    go mod download

COPY . .
RUN --mount=type=cache,target=/go/pkg/mod 
    --mount=type=cache,target=/root/.cache/go-build 
    CGO_ENABLED=0 go build -o /server ./cmd/server

最小化层数

每个 RUN 指令都会创建一个新层。组合相关命令可以减少层数并避免孤立文件:

# 不良: 3 层,apt 缓存保留在第 1 层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# 良好: 1 层,apt 缓存在同一层中清理
RUN apt-get update && 
    apt-get install -y --no-install-recommends curl && 
    rm -rf /var/lib/apt/lists/*

衡量您的进展

使用 docker imagesdocker history 来了解您的镜像大小来源:

# 查看镜像大小
docker images myapp
# REPOSITORY   TAG       SIZE
# myapp        latest    89MB

# 查看层分解
docker history myapp:latest
# IMAGE          SIZE      CREATED BY
# abc123         4.2MB     COPY --from=builder /app/dist ./dist
# def456         32MB      COPY --from=deps /app/node_modules ...
# ghi789         52MB      /bin/sh -c #(nop) ADD file:... (base)

# 使用 dive 进行交互式层探索
# https://github.com/wagoodman/dive
dive myapp:latest

工具 dive 特别有用 — 它可以精确显示每个层添加的文件,并突出显示在一个层中添加但在后续层中删除的文件所浪费的空间。

每个 Dockerfile 的检查清单

  • 从最小的基础镜像开始(alpine、slim、distroless 或 scratch)
  • 使用多阶段构建来分离构建和运行时
  • 在源代码之前复制依赖清单(利用层缓存)
  • 在运行时阶段只安装生产依赖
  • 创建 .dockerignore 文件
  • 以非 root 用户身份运行
  • 合并 RUN 命令以最小化层数
  • 从编译的二进制文件中剥离调试符号
  • 为包管理器使用 BuildKit 缓存挂载
  • 使用 docker scouttrivy 扫描最终镜像

更小的镜像不仅仅是一个锦上添花的优化。它们部署更快,存储成本更低,默认更安全,并且在出现问题时更容易调试。你花 30 分钟优化 Dockerfile 的时间,将在每次拉取、推送或扫描该镜像时得到回报。

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed