Docker 多阶段构建优化实践:从 1.5GB 到 150MB 的蜕变

By | 2026年6月20日

背景

在微服务架构盛行的今天,Docker 已成为应用部署的标准方式。然而,很多开发者的 Docker 镜像体积臃肿,动辄几百 MB 甚至 GB 级别,导致构建慢、传输慢、部署慢。以 Node.js 应用为例,一个简单的 Express 服务,如果直接使用 node:18 基础镜像,体积大约 1.5GB。但通过多阶段构建,我们可以将其缩小到 150MB 左右,同时保留运行所需的一切。

本文将从零开始,带你实践 Docker 多阶段构建,并分享一些独特的优化技巧。

什么是多阶段构建?

多阶段构建(Multi-stage Build)是 Docker 17.05 引入的特性。它允许在同一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 可以基于不同的基础镜像,并且只有最后一个阶段会生成最终镜像。这样,我们可以将构建环境和运行环境分离,只将必要的产物复制到最终镜像中。

实战:优化一个 Node.js 应用

我们以一个简单的 Node.js Web 应用为例,它使用 Express 框架,并且需要编译 TypeScript 和打包前端资源。

初始 Dockerfile(单阶段)


FROM node:18

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["node", "dist/index.js"]

这个镜像包含 node 运行时、所有开发依赖、源码、以及构建产物,体积约为 1.5GB。

优化第一步:多阶段构建

我们将构建过程拆分为两个阶段:

  • 构建阶段:使用 node:18 镜像,安装所有依赖(包括开发依赖),执行构建。
  • 运行阶段:使用更轻量的 node:18-alpine 镜像,仅复制构建产物和运行时依赖。

# 构建阶段
FROM node:18 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

# 运行阶段
FROM node:18-alpine

WORKDIR /app

# 复制构建产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 3000

CMD ["node", "dist/index.js"]

此时,镜像体积从 1.5GB 降至约 300MB。但还不够,因为 node_modules 中包含了大量开发依赖(如 TypeScript、测试框架等),这些在运行时并不需要。

优化第二步:分离依赖

更好的做法是,在构建阶段安装所有依赖,但仅复制运行时依赖到最终镜像。我们可以通过 npm ci --productionnpm prune --production 来实现。


# 构建阶段
FROM node:18 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

# 运行阶段
FROM node:18-alpine

WORKDIR /app

# 复制构建产物
COPY --from=builder /app/dist ./dist

# 复制 package.json 并仅安装生产依赖
COPY package*.json ./
RUN npm ci --production

EXPOSE 3000

CMD ["node", "dist/index.js"]

💡 注意npm ci --production 会安装 dependencies 中的包,忽略 devDependencies。确保你的 package.json 正确区分了运行时和开发依赖。

现在镜像体积降至约 150MB。

优化第三步:利用缓存加速构建

Docker 构建时,每层都会缓存。我们可以通过调整指令顺序,最大化利用缓存。例如,先复制 package.json 并安装依赖,再复制源码,这样只要 package.json 不变,依赖层就会被缓存。


# 构建阶段
FROM node:18 AS builder

WORKDIR /app

# 先复制依赖文件,安装依赖(利用缓存)
COPY package*.json ./
RUN npm install

# 再复制源码
COPY . .
RUN npm run build

# 运行阶段
FROM node:18-alpine

WORKDIR /app

# 复制构建产物
COPY --from=builder /app/dist ./dist

# 安装生产依赖
COPY package*.json ./
RUN npm ci --production

EXPOSE 3000

CMD ["node", "dist/index.js"]

优化第四步:使用更小的基础镜像

node:18-alpine 已经很小,但我们可以更进一步:使用 scratchdistroless 镜像。不过,Node.js 应用需要 glibc 等系统库,所以通常选择 alpineslim

对于静态文件服务,甚至可以不用 Node 镜像,直接用 Nginx。

高级技巧:多阶段构建的更多应用

1. 构建多个产物

如果你需要构建前端和后端,可以在一个 Dockerfile 中完成:


# 前端构建
FROM node:18 AS frontend-builder
WORKDIR /app
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build

# 后端构建
FROM node:18 AS backend-builder
WORKDIR /app
COPY backend/package*.json ./
RUN npm install
COPY backend/ ./
RUN npm run build

# 运行阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=backend-builder /app/dist ./dist
COPY --from=frontend-builder /app/dist ./public
CMD ["node", "dist/index.js"]

2. 使用缓存挂载(BuildKit)

Docker 18.09+ 支持 BuildKit,可以使用 --mount=type=cache 加速依赖安装:


# syntax=docker/dockerfile:1
FROM node:18 AS builder
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm \
    npm install
COPY . .
RUN npm run build

这会将 npm 缓存持久化,避免每次构建都重新下载。

3. 安全最佳实践

  • 运行阶段使用非 root 用户:

FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# ...
  • 避免复制敏感文件(如 .env),使用 Docker 的 .dockerignore

常见坑与踩坑经验

  • 坑1:忘记复制 package.json 导致 npm ci 失败。
  • 坑2npm ci 需要 package-lock.json,确保它存在于源码中。
  • 坑3:多阶段构建中,阶段名称必须唯一,否则会报错。
  • 坑4:Alpine 镜像中,某些原生模块可能需要编译,需要安装 build-base

总结

通过多阶段构建,我们成功将镜像体积从 1.5GB 压缩到 150MB,同时提升了构建速度和安全性。关键要点:

  1. 分离构建和运行环境
  2. 只复制必要文件
  3. 利用缓存优化构建速度
  4. 选择合适的基础镜像

下一步,你可以探索 Docker BuildKit 的高级特性,或者尝试将镜像体积压缩到 50MB 以下(使用 distroless 或 scratch)。

延伸阅读