Docker 与 Nginx 部署深度话题
多阶段构建 · compose 编排 · Nginx 反代/负载 · history 404 · 前端部署 · 跨域代理
🎬 怎么引出
「部署我有独立交付经验 — 多次客户现场部署,处理环境差异、服务依赖、网络策略。镜像优化用多阶段构建把前端镜像从 1G 降到 30M(build 阶段用 node,产物拷到 nginx 阶段)。Vue history 模式刷新 404 踩过坑,Nginx 要配 try_files fallback 到 index.html。跨域开发用 Vite proxy、生产用 Nginx 反代。部署是全栈基本功,简历有"独立部署"很加分。」
1
Docker 多阶段构建(镜像瘦身)
⚠️ 单阶段构建的痛
前端镜像如果直接 FROM node,把整个 node_modules + 源码都打进去,镜像 1G+。又大又慢,还带源码(不安全)。
多阶段构建: build 阶段用 node 编译出 dist,第二阶段用 nginx 只 COPY dist。最终镜像只有 nginx + 静态文件,30M 左右。
📋 前端多阶段 Dockerfile
# 阶段1:构建(用 node)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # 比 npm install 快且稳定
COPY . .
RUN npm run build # 产物在 /app/dist
# 阶段2:运行(只拿 dist,用 nginx)
FROM nginx:alpine
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;"]
# 最终镜像 ~30M,不含源码和 node_modules
关键优化: ① 多阶段(builder + runtime);② alpine 基础镜像(比完整版小很多);③ npm ci(比 install 快);④ .dockerignore 排除 node_modules(别 COPY 进去);⑤ 层缓存(COPY package.json 先 RUN install,再 COPY 源码,改代码不重装依赖)。
2
docker-compose 编排多容器
📋 前端 + 后端 + MySQL + Redis 一键启动
version: '3.8'
services:
frontend:
build: ./frontend
ports: ["80:80"]
depends_on: [backend]
backend:
build: ./backend
ports: ["8080:8080"]
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on: [mysql, redis]
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: xxx
MYSQL_DATABASE: app
volumes:
- mysql_data:/var/lib/mysql # 数据持久化
redis:
image: redis:alpine
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
关键点: ① depends_on 只管启动顺序,不管"就绪"(MySQL 可能还没接受连接,后端连不上),要配 healthcheck 或重试;② volumes 持久化(容器删了数据还在);③ 服务间用服务名通信(backend 连 mysql 用 hostname=mysql,Docker 内置 DNS);④ 环境差异用 .env 文件 + environment 注入。
3
Nginx 核心配置
📋 反向代理 + 负载均衡 + gzip + history
server {
listen 80;
server_name example.com;
# 前端静态资源
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # 关键!history 模式
}
# 反向代理后端 API + 负载均衡
location /api/ {
proxy_pass http://backend_upstream; # 见下面 upstream
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# gzip 压缩
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1000;
# 静态资源长缓存
location ~* \.(js|css|png|jpg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# 负载均衡:多个后端实例
upstream backend_upstream {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
# 可配权重/轮询/ip_hash(会话保持)
}
4
Vue history 模式 404(经典坑)
⚠️ 现象
Vue Router 用 history 模式,首页正常,但刷新 /user/profile 这种子路由就 404。开发环境没事,部署到 Nginx 就 404。
根因: history 模式 URL 是真实路径,Nginx 收到 /user/profile 会去找这个文件,找不到就 404。但 SPA 所有路由都该走 index.html(JS 接管路由)。解法: Nginx 配
try_files $uri $uri/ /index.html — 找不到的都回退到 index.html。
hash vs history: hash 模式 URL 带 #(如 /#/user),# 后不会发请求,没 404 问题,但不好看。history 模式干净,但要服务端配合 fallback。现代项目都用 history + Nginx fallback。
5
前端部署方案
📋 静态资源部署(主流方案)
- 静态资源(JS/CSS/图片)上 OSS/COS 对象存储,挂 CDN 加速
- HTML 不缓存(
Cache-Control: no-cache),保证拿到最新 index.html 引用新 hash 的 JS - JS/CSS 文件名带 hash(Vite/Webpack 默认),内容变 hash 变,可长缓存(
immutable) - 灰度发布:多版本并存,按 IP/cookie 灰度切流
- 回滚:旧版本不删,出问题切回(改 index.html 引用)
缓存陷阱: 如果 HTML 也缓存了,用户拿到旧 HTML 引用的 JS hash 已不存在 → 白屏。铁律:HTML 不缓存,带 hash 的资源长缓存。
6
跨域处理(开发 vs 生产)
开发环境:Vite/Webpack proxy
- 前端 dev server 转发 /api 到后端
- 浏览器以为是同源,不发 OPTIONS
- vite.config.js 配 server.proxy
生产环境:Nginx 反代
- 前端和 /api 同域(Nginx 转发)
- 彻底没有跨域问题
- 或后端配 CORS 头
最佳实践: 生产用 Nginx 反代让前后端同域(/ 是前端,/api/ 转后端),彻底无跨域。开发用 proxy。别在生产依赖后端 CORS 头(多一次 OPTIONS 预检,且暴露后端地址)。
7
高频追问
1Docker 镜像怎么减小体积? 高频
① 多阶段构建(build 用 node,运行用 nginx/alpine);② alpine 基础镜像(比完整版小);③ .dockerignore 排除 node_modules;④ 合并 RUN(每条 RUN 一层,合并减层);⑤ npm ci。前端镜像能从 1G 降到 30M。
2Vue history 模式刷新 404 怎么解决? 必问
history 模式 URL 是真实路径,Nginx 找不到子路由文件就 404。解法:Nginx 配 try_files $uri $uri/ /index.html,找不到的回退到 index.html(JS 接管路由)。hash 模式没这问题但 URL 不好看。
3Nginx 负载均衡策略有哪些? 中等
① 轮询(默认);② 权重(weight,性能强的多分);③ ip_hash(同 IP 固定到一台,会话保持);④ least_conn(最少连接);⑤ fair(按响应时间,需第三方模块)。upstream 配 server 列表。
4跨域怎么解决? 必问
① 开发: Vite/Webpack proxy 转发;② 生产: Nginx 反代让前后端同域(推荐,无跨域);③ 后端 CORS 头(Access-Control-Allow-Origin,简单但多 OPTIONS 预检);④ JSONP(老,只 GET)。生产优先 Nginx 同域。
5前端缓存策略怎么做? 高频
HTML 不缓存(no-cache,拿最新引用),带 hash 的 JS/CSS 长缓存(immutable,1 年)。文件名 hash 变 = 内容变 = 浏览器重新下载;hash 不变用缓存。这样改版用户自动拿新版,不改版秒开。铁律:HTML 不缓存,资源长缓存。
6docker-compose 的 depends_on 能保证服务就绪吗? 中等
不能。 depends_on 只管启动顺序,不管"服务能接受连接了"(MySQL 启动了但还没 ready)。解法:① 配 healthcheck(depends_on condition: service_healthy);② 应用层重试连接(Spring Boot retry);③ 用 wait-for-it.sh 等工具等待端口。
💬 这坑踩过说明真用过 compose。
8
30 秒陈述
🎤 30 秒版
「部署我有独立交付经验,多次客户现场部署。Docker 用多阶段构建把前端镜像从 1G 降到 30M(build 用 node,运行用 nginx-alpine)。compose 编排前端+后端+MySQL+Redis,服务间用服务名通信,数据 volumes 持久化。Nginx 核心是反代 + try_files 解决 history 404 + gzip + 静态资源长缓存。缓存铁律:HTML 不缓存,带 hash 的 JS/CSS 长缓存。跨域生产用 Nginx 同域反代,开发用 Vite proxy。我有客户现场部署经验,处理过环境差异、网络策略这些问题。」