引言
把 Vite 应用部署到生产,最干净的形态是「一个可复现的镜像」:docker build 出包含构建产物 + 静态服务器的镜像,随处可跑、可回滚、可审计。但 Docker 用不好就成了灾难——每次构建全量重装依赖、镜像几个 GB、层缓存失效。本文用 多阶段构建 把「构建」与「运行」分离,用 依赖层缓存 让 CI 秒级复用,用 nginx 配置 处理好 SPA 路由与缓存,最后给出体积优化与安全加固清单。
前置:https://plumephp.com/vite-env-production-best-practices/(生产构建)、https://plumephp.com/vite-ci-cd-optimization/(CI/CD)、https://plumephp.com/vite-config-guide/(构建配置)。
目录
- 1. 为什么容器化
- 2. 多阶段构建架构
- 3. 依赖层缓存
- 4. 构建产物阶段
- 5. 静态服务镜像
- 6. nginx 与 SPA 路由
- 7. CI 中的缓存复用
- 8. 镜像体积优化
- 9. 安全与多环境
- 10. 速查表与一句话记忆
- 延伸阅读
1. 为什么容器化
对比「直接在服务器上 npm run build + 起服务」:
| 维度 | 裸部署 | 容器化 |
|---|---|---|
| 可复现 | 依赖环境漂移 | 镜像锁定一切 |
| 回滚 | 手动 | docker run 旧镜像 |
| 多环境 | 每台配环境 | 同一镜像多标签 |
| 可审计 | 无 | 镜像层可查 |
| 交付 | 手动/脚本 | 标准 registry |
工程定位:容器化不是「更复杂」,而是把「部署方式」变成「标准产物」。前端尤其适合——产物是纯静态文件,静态服务镜像可以极简。
2. 多阶段构建架构
多阶段构建(multi-stage)的核心:构建工具与运行环境分离,只把产物带进最终镜像。
# ---- 阶段 1:构建 ----
FROM node:20-alpine AS builder
WORKDIR /app
# 依赖安装(见第 3 节缓存技巧)
COPY pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# 复制源码 + 构建
COPY . .
RUN pnpm build # 产出 dist/
# ---- 阶段 2:运行(只含产物 + 静态服务器)----
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
为什么分两阶段:
- builder 镜像大(Node + 全部依赖,数百 MB),但只在构建时存在;
- 运行镜像小(nginx + 产物,几十 MB),且不含任何构建工具;
- 依赖的构建工具/密钥不会带进生产镜像。
3. 依赖层缓存
Docker 缓存的单位是「层」——文件没变,层就复用。依赖层缓存的技巧是「把锁文件单独 COPY」:
# 好:锁文件先 COPY,层缓存命中时跳过 install
COPY pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile # 锁文件没变 → 这层缓存命中
COPY . . # 源码变更只影响这层开始
层缓存逻辑:
COPY pnpm-lock.yaml → install → COPY . → build
│ 锁文件没变 │ 缓存命中 │ 源码变了 │ 缓存失效
▼ ▼ ▼ ▼
复用 复用 重新复制 重新构建
注意 pnpm 的特殊性:pnpm 的符号链接结构,COPY . . 会覆盖 node_modules 的符号链接。最佳实践是「先 COPY 锁文件 install,再 COPY 源码」;若源码里不含 node_modules,缓存才有效。
4. 构建产物阶段
构建阶段要「又快又可复现」:
FROM node:20-alpine AS builder
WORKDIR /app
# 1. 只复制依赖声明,最大化缓存
COPY pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile
# 2. 复制源码(此层会随源码变更失效)
COPY . .
# 3. 环境变量:用 ARG 注入 VITE_ 前缀变量
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
# 4. 构建(用缓存挂载加速)
RUN --mount=type=cache,target=/app/node_modules/.cache \
pnpm build
关键点:
--frozen-lockfile:CI 与构建必须锁版本,杜绝「构建时依赖漂移」;- 构建缓存挂载:
--mount=type=cache让 Rollup/esbuild 的缓存跨构建复用; ARG注入环境:VITE_前缀变量在构建期打进产物(https://plumephp.com/vite-env-production-best-practices/);.dockerignore:排除 node_modules、dist、.git,防止 COPY 进无关文件破坏缓存。
5. 静态服务镜像
运行镜像的选择影响体积与性能:
| 基镜像 | 体积 | 特点 |
|---|---|---|
nginx:alpine | ~50MB | 标准静态服务器,性能好 |
caddy | ~40MB | 自动 HTTPS,配置简单 |
node 裸跑静态服务器 | ~100MB+ | 含 Node,非最优 |
| 纯静态(busybox/httpd) | ~10MB | 极简,功能受限 |
# 推荐:nginx:alpine
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 非 root 用户运行(安全)
USER nginx
工程要点:运行镜像不需要 Node——产物是静态文件,任何静态服务器都能喂。选最小 + 支持好 SPA 路由的基镜像即可。
6. nginx 与 SPA 路由
SPA 的「伪路由」(如 /about 无对应文件)需要 nginx 回退到 index.html:
server {
listen 80;
root /usr/share/nginx/html;
# 静态资源缓存(带 hash 的产物名可长缓存)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA 回退:找不到文件的路径 → index.html
location / {
try_files $uri $uri/ /index.html;
}
}
try_files 逻辑:
/ → index.html
/assets/x.js → 直接返回文件
/about → 无此文件 → /index.html(前端路由接管)
缓存纪律:
- 带 hash 的资源(
assets/main-abc123.js):长缓存(immutable); index.html:no-cache(每次校验,保证新版本快速生效);- API 不在 nginx 层:
/api用location /api/ { proxy_pass }或走网关。
7. CI 中的缓存复用
CI 里 Docker 构建的最大痛点是「缓存不跨构建」。解决:
方案 1:GitHub Actions + BuildKit 内联缓存
方案 2:远程缓存(registry)— 每次 push 的层可被复用
# GitHub Actions(示例)
- name: Build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/team/app:${{ github.sha }}
cache-from: type=registry,ref=ghcr.io/team/app-cache
cache-to: type=registry,ref=ghcr.io/team/app-cache,mode=max
关键收益:mode=max 保存所有层(含中间层),下一次构建只有「源码层」重做,依赖层直接命中——CI 构建从分钟级降到秒级。
8. 镜像体积优化
镜像瘦身是「部署速度 + 攻击面」双赢:
体积优化清单:
□ 多阶段构建(不把 Node 带进运行镜像)
□ 使用 alpine 基镜像(体积小、安全更新快)
□ .dockerignore 排除无关文件
□ pnpm 只装生产依赖(构建后 prune)
□ 产物压缩(gzip/brotli 预压缩,nginx 直接喂)
□ 清理缓存(pnpm store 用 --mount=type=cache 隔离)
# 预压缩产物:nginx 直接提供 .gz/.br,节省 CPU
RUN --mount=type=cache,target=/root/.cache \
pnpm build && \
(cd dist && for f in assets/*.js assets/*.css; do gzip -9 -k "$f"; done)
度量:镜像体积进入 CI 报告——超阈值告警,防止「悄悄变大」。
9. 安全与多环境
容器化部署的安全与多环境策略:
安全清单:
□ 非 root 运行(USER nginx / node)
□ 只安装生产依赖(无构建工具)
□ 镜像内容审计(不含密钥/源码 map)
□ 漏洞扫描(trivy / docker scan)
□ 基础镜像定期更新(拉取安全修复)
多环境:
□ 同一镜像,多 tag(:prod / :staging / :test)
□ 运行时环境变量(注入 API 地址),构建期 ARG 尽量少
# 多环境 tag 管理
docker build -t app:staging -t app:prod .
docker push app:staging && docker push app:prod
工程要点:运行时能注入的配置(API URL、功能开关)尽量运行时注入,避免「每个环境一个镜像」的镜像爆炸;只有「构建期决定」的东西(如 VITE_ 前缀变量)才用 ARG。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 为什么分阶段 | 构建与运行分离,产物镜像极简 |
| 依赖缓存怎么命中 | 锁文件单独 COPY,不变则层缓存命中 |
| 运行镜像用什么 | nginx:alpine + 产物,不需 Node |
| SPA 路由怎么处理 | try_files ... /index.html 回退 |
| CI 缓存怎么复用 | BuildKit cache-from/to 远程缓存 |
| 镜像怎么变小 | 多阶段 + alpine + 预压缩 + dockerignore |
| 安全怎么保障 | 非 root + 漏洞扫描 + 运行时注入配置 |
一句话记忆:Vite 容器化 = 多阶段(构建→nginx)+ 依赖层缓存(锁文件优先)+ SPA 回退(try_files)+ BuildKit 远程缓存 + 镜像瘦身(alpine/预压缩)+ 运行时注入配置——「一个镜像,处处可跑,构建秒级」。
延伸阅读
- https://plumephp.com/vite-env-production-best-practices/ — 生产构建与环境变量
- https://plumephp.com/vite-ci-cd-optimization/ — CI/CD 流水线与部署
- https://plumephp.com/vite-build-optimization/ — 产物体积与代码分割
- https://plumephp.com/vite-security-csp-hardening/ — 容器内 nginx 的 CSP 头
- Docker 专题 — Docker 镜像与容器实践
- DevOps 专题 — CI/CD 与基础设施
- 网络专题 — 静态资源与 CDN 缓存
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。