Docker 多阶段构建实战:从 1.2GB 到 12MB
一个静态站镜像的瘦身过程
一个静态博客的镜像有多大才算合理?答案是 12MB。但如果你只用单阶段构建,它会是 1.2GB。
单阶段的浪费
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npx", "serve", "dist"]
问题很明确:镜像里带着 node_modules(几百 MB)、Node 运行时本身、npm 缓存、源码、还有构建工具链。而这些在运行阶段一个都用不到。
产物只是 dist/ 目录里的静态 HTML 和资源。
改成多阶段
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
最终镜像里只有 Nginx、HTML 和资源。没有 Node,没有源码,没有依赖漏洞要修。
三个细节
1. 先拷贝 package.json
COPY package*.json ./
RUN npm ci
COPY . .
只有依赖清单变化时才会失效重建层。顺序反过来的话每次改代码都要重装全部依赖。
2. 用 npm ci 不用 npm install
npm ci 严格按 lock 文件安装,且会删掉已有的 node_modules,构建结果可复现。
3. Nginx 用 COPY --from
跨阶段复制不会把 builder 阶段的中间层带进最终镜像。
缓存策略
| 层 | 何时失效 | 优化后 |
|---|---|---|
| 依赖安装 | package.json 变化 | 极少 |
| 源码拷贝 | 任意文件变化 | 频繁 |
npm run build | 源码变化 | 频繁 |
依赖安装层稳定后,本地构建通常 10 秒内完成。
值得注意的一点
镜像瘦身不只是为了省磁盘。最终镜像里没有 Node 运行时,也意味着没有 Node 生态的 CVE 需要你跟踪。攻击面直接归零,这对一个只提供静态内容的站点来说是质变。