一、从后端开发者的直觉困惑说起
Spring Boot 项目的部署流程非常简单:
mvn clean package-DskipTests# 产出 app.jardockerbuild-tmy-app.# 打包成镜像dockerrun my-app# 跑起来,浏览器就能访问了于是自然产生一个疑问:Vue/React 项目不能这样吗?npm run build也有产物(dist/文件夹),为什么不能直接docker run?
npmrun build# 产出 dist/ 文件夹dockerrun my-app# ❌ 容器直接死掉,什么也访问不了答案藏在"产物"的本质差异里。
二、关键差异:产物里有没有 HTTP 服务器
| 后端 jar 包 | 前端 dist 文件夹 | |
|---|---|---|
| 产物是什么 | app.jar(可执行文件) | index.html+.js+.css(纯数据文件) |
| 包含 HTTP 服务器吗 | ✅ 内嵌 Tomcat | ❌ 不包含任何服务器 |
| 能监听端口吗 | ✅ Tomcat 自动监听 8080 | ❌ 没有进程监听 |
| 能响应 HTTP 请求吗 | ✅ Controller 在等请求 | ❌ 没有人收请求 |
用后端术语类比:dist/文件夹本质上等于src/main/resources/static/下的静态文件——它们是数据,不是服务。
Spring Boot 里放个index.html能访问,不是因为 HTML 自己能跑,而是因为Tomcat在对外暴露。把 Tomcat 去掉,那个 HTML 文件就是硬盘上一段字节,TCP 连接都建不起来。
Vuedist/文件夹,就是去掉了 Tomcat 的那些静态文件。
三、Docker 的运行本质
很多开发者对 Docker 有一个常见误解:以为它是一个"虚拟机",里面放了文件就能跑。
Docker 启动的是进程,进程死了容器就死。
# 这样写没有意义: FROM alpine COPY ./dist /app # 光拷贝文件,没有任何进程要跑docker run之后,容器里一个进程都没有,Docker 发现没有前台进程 → 容器立刻退出。就算强行不让它退出,80 端口也是空的——没有程序在那里调用socket.listen(80),TCP 握手都完成不了。
核心矛盾:curl http://localhost:80/这个请求,得有人在 80 端口执行recv()才行。那个人在哪?
四、Nginx 就是那个"服务员"
Nginx 是一个持续运行的 C 程序,做了三件事:
1. 启动后不退出(进程活着 → 容器不死) 2. 调用 socket.listen(80) 监听 80 端口(有人接电话了) 3. 收到 HTTP 请求 → 读硬盘上的文件 → 返回给浏览器放进 Dockerfile:
FROM nginx:latest # 基础镜像自带 nginx 程序 COPY ./dist /usr/share/nginx/dist # 前端文件放进 nginx 的静态目录执行docker run后,nginx 进程跑起来 → 监听 80 端口 → 浏览器访问http://localhost/index.html→ nginx 读取/usr/share/nginx/dist/index.html→ 返回文件内容。
请求链路:
浏览器 容器 │ │ │ GET /index.html │ │ ──────────────────────────────→ │ nginx 进程 (在 80 端口 listen) │ │ │ │ 1. accept() 建立连接 │ │ 2. 解析 HTTP,拿到路径 "/index.html" │ │ 3. open() 读硬盘文件 │ │ 4. send() 把字节塞进 HTTP 200 响应 │ │ │ ←── HTTP 200 + <html>... │ │ │五、为什么不用 webpack-dev-server?
npm run dev启动的开发服务器(webpack-dev-server)在本地调试时确实在localhost:8080提供服务。但它绝对不能上生产:
| webpack-dev-server | nginx | |
|---|---|---|
| 设计目的 | 本地开发调试 | 生产环境 |
| 热更新 | 有(每次改动自动刷新) | 不需要 |
| 性能模型 | Node.js 单线程,一个请求走 webpack 编译管线 | C 语言 epoll,直接从 OS page cache 读文件 |
| 内存 | 300MB+ | ~5MB |
| 并发能力 | 几百 | 数万 |
webpack-dev-server 上生产 = 用自行车跑高速。不是走不通,是性能和安全都撑不住。
六、Nginx 的另一项关键能力:SPA 路由回退
Vue Router 在history模式下,用户访问/contract/detail/123,但dist/文件夹里根本没有contract/detail/123.html这个文件。为什么不是 404?
因为路由解析在浏览器端完成。服务器面对任何路径,只需要返回index.html,然后 Vue Router 在浏览器里看到路径,再决定渲染哪个组件。
这需要 nginx 配一条关键规则:
location / { try_files $uri $uri/ @rewrites; # 先尝试找实际文件 } location @rewrites { rewrite ^(.+)$ /index.html last; # 找不到就统一返回 index.html }翻译:“磁盘上没这个文件?那就返回 index.html,让前端路由自己看着办。”
没有这条配置,用户刷新/contract/detail/123页面,nginx 老老实实去找文件,找不到就返回 404——这就是前端开发中经典的"SPA 刷新 404 问题"。
七、前后端对比,一张图总结
后端 Java 应用: ┌────────────────────────────┐ │ app.jar │ │ ┌──────────────────────┐ │ │ │ Tomcat (服务员) │ │ ← jar 包自带的 │ │ 监听 8080 端口 │ │ │ │ 执行 Controller │ │ │ └──────────────────────┘ │ │ ┌──────────────────────┐ │ │ │ static/ 静态资源 │ │ ← 没有 Tomcat,这些也是死文件 │ └──────────────────────┘ │ └────────────────────────────┘ 前端 Vue 应用: ┌────────────────────────────┐ │ Docker 镜像 │ │ ┌──────────────────────┐ │ │ │ nginx (服务员) │ │ ← 得自己配一个 │ │ 监听 80 端口 │ │ │ └──────────────────────┘ │ │ ┌──────────────────────┐ │ │ │ dist/ 前端文件 │ │ ← 只有文件,没有服务器 │ └──────────────────────┘ │ └────────────────────────────┘核心结论:后端 jar 包自带"服务员"(内嵌 Tomcat);前端 build 出来只有"菜单"没有"服务员",所以必须配一个 nginx 来当服务员。两者的部署流水线逻辑完全一致,区别只在于"集装箱里装的是谁"。