纸上山河 记录技术、思考与生活

Docker 多阶段构建实战:从 1.2GB 到 12MB

一个静态站镜像的瘦身过程

约 1 分钟 making

一个静态博客的镜像有多大才算合理?答案是 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 需要你跟踪。攻击面直接归零,这对一个只提供静态内容的站点来说是质变。