Docker 多阶段构建优化实战:从 1.2GB 到 80MB 的镜像瘦身指南

By | 2026年10月14日

为什么你的 Docker 镜像总是这么大?

很多团队在容器化应用时,常常直接使用 node:18 这样的基础镜像,然后把源代码、依赖、构建工具全部塞进去。结果一个简单的 Express 应用镜像轻松突破 1GB,不仅拖慢 CI/CD 速度,还增加了安全风险。

多阶段构建(Multi-stage Build)是 Docker 17.05 引入的特性,它允许你在一个 Dockerfile 中定义多个阶段,每个阶段可以使用不同的基础镜像,最终只将必要的产物复制到最终镜像中。本文将通过一个真实的前后端项目(React + Node.js),带你一步步优化镜像,并分享我在生产环境中总结的 5 个关键技巧。

项目背景与初始 Dockerfile

假设我们有一个典型的全栈项目:


my-app/
├── client/          # React 前端(Vite 构建)
│   ├── src/
│   ├── package.json
│   └── vite.config.js
├── server/          # Node.js 后端(Express)
│   ├── src/
│   └── package.json
└── docker-compose.yml

最初的 Dockerfile 可能是这样的(单阶段):


FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "server/src/index.js"]

构建后镜像大小:1.2GB。问题很明显:包含了完整的 Node.js 镜像、devDependencies、源代码、构建缓存等。

第一步:拆分构建阶段与运行阶段

多阶段构建的核心思想是:构建环境与运行环境分离。我们定义两个阶段:

  • builder 阶段:安装所有依赖(包括 devDependencies),执行构建
  • runner 阶段:仅复制构建产物和 production 依赖

优化后的 Dockerfile:


# 阶段1:构建
FROM node:18-alpine AS builder
WORKDIR /app

# 先复制 package.json 以利用缓存
COPY package*.json ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/

RUN npm ci --workspaces --include-workspace-root

# 复制源代码并构建
COPY . .
RUN npm run build --workspace=client

# 阶段2:运行
FROM node:18-alpine AS runner
WORKDIR /app

# 只复制生产依赖和构建产物
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/server/package*.json ./server/
RUN npm ci --omit=dev --workspaces --include-workspace-root

COPY --from=builder /app/server/src ./server/src
COPY --from=builder /app/client/dist ./client/dist

EXPOSE 3000
CMD ["node", "server/src/index.js"]

构建后镜像大小:约 350MB。已经大幅减小,但还有优化空间。

> 💡 注意:npm ci 比 npm install 更快且更可靠,它会严格遵循 package-lock.json。使用 --workspaces 可以处理 monorepo 结构。

第二步:使用更小的基础镜像与清理缓存

node:18-alpine 本身约 170MB,我们可以进一步减小。但 Alpine 使用 musl libc,某些原生模块可能不兼容。一个折中方案是使用 node:18-slim(基于 Debian,约 240MB),或者继续用 Alpine 但确保依赖兼容。

此外,npm ci 会产生缓存文件,我们可以通过设置 npmconfigcache 到临时目录并清理来减少层大小:


FROM node:18-alpine AS builder
WORKDIR /app

# 设置 npm 缓存到 /tmp,避免污染镜像层
ENV npm_config_cache=/tmp/npm-cache

COPY package*.json ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/

RUN npm ci --workspaces --include-workspace-root

COPY . .
RUN npm run build --workspace=client

# 清理缓存(虽然多阶段构建中不必须,但能减少中间层大小)
RUN rm -rf /tmp/npm-cache

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/package*.json ./
COPY --from=builder /app/server/package*.json ./server/
RUN npm ci --omit=dev --workspaces --include-workspace-root && \
    npm cache clean --force

COPY --from=builder /app/server/src ./server/src
COPY --from=builder /app/client/dist ./client/dist

# 使用非 root 用户运行
USER node

EXPOSE 3000
CMD ["node", "server/src/index.js"]

镜像大小:约 120MB。

第三步:使用 distroless 或 scratch 镜像(进阶)

对于追求极致体积的场景,可以考虑 Google 的 distroless 镜像,它只包含应用和运行时依赖,没有 shell 和包管理器,安全性极高。


FROM node:18-alpine AS builder
# ... 同上构建步骤 ...

FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app

COPY --from=builder /app/package*.json ./
COPY --from=builder /app/server/package*.json ./server/
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server/src ./server/src
COPY --from=builder /app/client/dist ./client/dist

EXPOSE 3000
CMD ["server/src/index.js"]

注意:distroless 镜像中 node 命令是入口点,所以 CMD 直接写脚本路径。镜像大小可降至 约 80MB。

> ⚠️ 坑点:distroless 没有 shell,无法进入容器调试。建议仅在稳定环境使用,并确保日志输出到 stdout。

第四步:优化构建缓存与 CI 加速

多阶段构建的另一个优势是充分利用缓存。以下是几个关键实践:

  1. 依赖与源码分离:先复制 package.json 并安装依赖,再复制源码。这样修改源码不会触发依赖重装。
  2. 使用 BuildKit:Docker 18.09+ 支持 BuildKit,可以并行构建阶段,并支持 --mount=type=cache 持久化缓存。

启用 BuildKit 并利用缓存挂载:


# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/

RUN --mount=type=cache,target=/root/.npm \
    npm ci --workspaces --include-workspace-root

COPY . .
RUN npm run build --workspace=client

# ... 运行阶段同上

构建命令:DOCKER_BUILDKIT=1 docker build -t my-app .

这样在 CI 中,npm 缓存可以跨构建复用,大幅减少安装时间。

第五步:安全与体积的额外技巧

  • 合并 RUN 指令:减少镜像层数,但可读性会下降,需权衡。
  • 使用 .dockerignore:排除 node_modules、.git、测试文件等,避免复制无用内容。
  • 多平台构建:使用 docker buildx 构建 arm64/amd64 镜像,便于在 Apple Silicon 和云服务器上运行。

示例 .dockerignore:


**/node_modules
**/dist
**/.git
**/*.md
**/Dockerfile
**/docker-compose.yml

完整 Dockerfile 与验证

最终优化后的 Dockerfile(使用 BuildKit 和 distroless):


# syntax=docker/dockerfile:1.4

# ---- 构建阶段 ----
FROM node:18-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/

RUN --mount=type=cache,target=/root/.npm \
    npm ci --workspaces --include-workspace-root

COPY . .
RUN npm run build --workspace=client

# ---- 运行阶段 ----
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app

COPY --from=builder /app/package*.json ./
COPY --from=builder /app/server/package*.json ./server/
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server/src ./server/src
COPY --from=builder /app/client/dist ./client/dist

EXPOSE 3000
CMD ["server/src/index.js"]

构建并查看大小:


DOCKER_BUILDKIT=1 docker build -t my-app:optimized .
docker images my-app:optimized

输出类似:


REPOSITORY   TAG         IMAGE ID       SIZE
my-app       optimized   abc123def456   82.3MB

总结

通过多阶段构建,我们成功将镜像从 1.2GB 压缩到 80MB 左右,同时提升了安全性和 CI 效率。关键点回顾:

  1. 分离构建与运行阶段,只复制必要产物。
  2. 使用 Alpine 或 distroless 减小基础镜像体积。
  3. 利用 BuildKit 缓存挂载加速构建。
  4. 遵循最小权限原则,使用非 root 用户。
  5. 编写 .dockerignore 避免上下文膨胀。

下一步,你可以探索:

  • 使用 docker scout 分析镜像漏洞
  • 结合 GitHub Actions 实现自动化构建与推送
  • 尝试 kaniko 在 Kubernetes 中构建镜像

希望这篇教程能帮你打造更轻量、更安全的容器镜像!