Nginx 反向代理:80 端口子路径部署静态站的配置要点
Nginx 反向代理:80 端口子路径部署静态站的配置要点
一台 ECS 只有一个 80 端口,但常常要放好几个东西:一个后台管理页、一个文档站、一个前端 Demo。最常见也最容易出问题的做法,就是用子路径区分——/app/、/docs/、/demo/。
这套方案看着简单,实际配置时踩的坑高度集中:proxy_pass 后面那个斜杠、root 和 alias 的差别、index.html 里的绝对路径、try_files 和 alias 打架。这篇把这几个点逐个讲透,最后给一份能直接用的完整配置。
一、先分清楚:什么是反代,什么是静态托管
同一个 location 里只能选一种处理方式:
- 静态托管:
root或alias指到磁盘目录,Nginx 自己读文件返回 - 反向代理:
proxy_pass转发给后端进程(Node、Java、Python),由后端返回
两者的路径拼接规则完全不同,混着记最容易乱。先各自说清楚。
静态托管:root 与 alias
# root 的规则:把完整 URI 拼到 root 后面
location /docs/ {
root /data/sites;
}
# 请求 /docs/guide.html → 读 /data/sites/docs/guide.html
# alias 的规则:把 location 匹配到的部分替换成 alias 路径
location /docs/ {
alias /data/sites/docs-current/;
}
# 请求 /docs/guide.html → 读 /data/sites/docs-current/guide.html
一句话记:root 是拼接,alias 是替换。alias 后面必须带斜杠,且 location 也必须带斜杠,否则路径会错位成 docs-currentguide.html 这种怪东西。
那为什么需要 alias?因为目录名和 URL 前缀经常不一致。上面这个例子,/docs/ 对应的是 docs-current/——发布时用软链或者换目录名做版本切换,URL 不变。这时候只有 alias 能做到。
反向代理:proxy_pass 末尾那个斜杠
这是最高频的坑,规则只有两条:
# 不带斜杠:原样转发完整 URI
location /app/ {
proxy_pass http://127.0.0.1:8080;
}
# 请求 /app/api/list → 后端收到 /app/api/list
# 带斜杠:剥掉 location 匹配的前缀
location /app/ {
proxy_pass http://127.0.0.1:8080/;
}
# 请求 /app/api/list → 后端收到 /api/list
判断标准是后端代码里路由怎么写的。 如果后端启动时就挂在根路径(app.get('/api/list')),用带斜杠的版本,把前缀剥掉;如果后端自己已经带了前缀(app.get('/app/api/list')),用不带斜杠的版本。
改这一处忘掉另一处,症状非常典型:页面能打开,但所有接口 404,而直接访问后端端口是正常的。
二、静态站放到子路径下,为什么样式全丢了
这是子路径部署最有代表性的问题。页面 200 打开了,但一片白底黑字,控制台一排 404。
原因很直接:构建产出的 index.html 里写的是绝对路径。
<script src="/assets/index-a1b2c3.js"></script>
<link rel="stylesheet" href="/assets/index-d4e5f6.css">
浏览器拿到这份 HTML,会按 http://你的域名/assets/index-a1b2c3.js 去请求——注意它不会带上 /app/ 前缀。而你只在 Nginx 里配了 /app/,根路径下没有这些文件,于是 404。
解决办法有三个,按推荐程度排:
方案一:构建时指定 base(推荐)
Vite 项目在 vite.config.js 里加:
export default defineConfig({
base: '/app/',
})
Webpack 项目用 output.publicPath = '/app/'。React(CRA)用环境变量 PUBLIC_URL=/app。
这样构建产物里的路径会变成 /app/assets/index-a1b2c3.js,天然正确,而且部署到根路径时把 base 改回 / 即可,不用动 Nginx。
方案二:HTML 里加 base 标签
如果拿到的产物不能重新构建(比如是别人给的包),可以在 <head> 里最靠前的位置插一行:
<base href="/app/">
<base> 会改变页面内所有相对路径的解析基准。注意两点:必须放在所有引用资源的标签之前;而且它只影响相对路径,src="/assets/x.js" 这种以斜杠开头的绝对路径依然会按域名根解析,不受影响。所以这条路只对"产物的路径本来就是相对的"有效。
方案三:Nginx 层重写(兜底,最不推荐)
location /assets/ {
alias /data/sites/app/assets/;
}
把根路径的请求也指过去。能用,但等于把 /assets/ 这个根路径占用了,第二个子站再想用 assets 就冲突。只在实在改不动产物时用。
三、try_files 与 alias 的经典冲突
单页应用(SPA)需要这样一段配置,让所有前端路由回退到 index.html:
location / {
try_files $uri $uri/ /index.html;
}
放到子路径下,直觉会这么写:
location /app/ {
alias /data/sites/app/;
try_files $uri $uri/ /app/index.html;
}
结果访问 /app/dashboard 报 500 或者 404 循环。原因是 try_files 里的 $uri 是完整的请求 URI(/app/dashboard),而 alias 已经把 location 前缀替换掉了,两者拼起来变成了 /data/sites/app/app/dashboard 这种不存在的路径。
三种可行写法,选一种:
# 写法一:用 root 代替 alias,让目录结构与 URL 前缀一致
location /app/ {
root /data/sites;
try_files $uri $uri/ /app/index.html;
}
# 需要把产物放到 /data/sites/app/ 下
# 写法二:保留 alias,回退目标给完整路径,并把 uri 部分显式剥离
location /app/ {
alias /data/sites/app/;
try_files $uri $uri/ /app/index.html;
}
# 注意:这个写法在 nginx 1.7 以后行为正常,
# 但老版本会有 $uri 不剥离前缀的问题,升级即可
# 写法三:拆成两段,静态资源走 alias,其余回退
location /app/assets/ {
alias /data/sites/app/assets/;
expires 30d;
}
location /app/ {
alias /data/sites/app/;
try_files $uri $uri/ /app/index.html;
}
我在生产上更倾向写法一:目录结构跟 URL 对齐,root 语义清晰,不会踩 alias 的隐式规则。alias 只留给"目录名必须与 URL 不同"的场景。
四、几个容易漏但会出事的小配置
index 指令
location 里如果没有 index,访问 /app/ 会返回 403 或目录列表:
location /app/ {
alias /data/sites/app/;
index index.html;
}
注意:alias 目录下使用 index 时,请求 /app(不带尾斜杠)会先 301 到 /app/,这是正常行为,不是 bug。
上传接口 413
Nginx 默认 client_max_body_size 1m,任何超过 1MB 的上传都会返回 413,而后端日志里什么都没有——因为请求根本没到后端:
location /app/api/ {
proxy_pass http://127.0.0.1:8080/api/;
client_max_body_size 20m;
}
gzip 默认只压 html
gzip on; 之后如果不写 gzip_types,只会压缩 text/html。JS 和 CSS 有好几百 KB,压不压差好几倍:
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
add_header 会被子级覆盖
这条很隐蔽:只要子 location 里写了任何一条 add_header,父级的所有 add_header 都会失效,不是合并而是覆盖。
server {
add_header X-Frame-Options SAMEORIGIN;
add_header Cache-Control "no-store";
location /app/assets/ {
add_header Cache-Control "public, max-age=2592000";
# 这里只剩这一条,父级两条都没了
}
}
要么在每个需要的 location 里写全,要么把公共头放到 server 层并且在子层不写 add_header。
反代要带上真实 Host 和客户端 IP
不写这几行,后端日志里全是 127.0.0.1,限流和审计都没法做:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
如果子路径下还有 WebSocket(比如实时日志、在线协作),必须额外三行,否则连接会一直停在 101 之前:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
五、可直接用的完整配置
假设:静态站放 /app/,后端 API 走 /app/api/,两个都要在 80 端口下。
server {
listen 80;
server_name example.com;
root /data/sites;
index index.html;
# 前端静态站
location /app/ {
try_files $uri $uri/ /app/index.html;
}
# 后端接口,剥掉 /app 前缀
location /app/api/ {
proxy_pass http://127.0.0.1:8080/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 20m;
}
# 静态资源长缓存
location /app/assets/ {
expires 30d;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_min_length 1k;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
}
对应目录结构:
/data/sites/app/
├── index.html
└── assets/
├── index-a1b2c3.js
└── index-d4e5f6.css
这里刻意用了 root 而不是 alias,前提就是产物放在 /data/sites/app/,和 URL 前缀一致。如果做不到目录对齐,再换 alias 并注意上一节的坑。
六、出问题时的三步排查
子路径部署的故障,九成能用这三步定位:
1. 先看响应头,判断请求到了哪一层
$ curl -I http://example.com/app/
HTTP/1.1 200 OK
Server: nginx/1.24.0
- 200:Nginx 静态托管正常,问题在前端产物路径
- 301:少了尾斜杠,属正常重定向,看
Location对不对 - 404:要么目录写错了,要么
try_files回退目标写错了 - 502:
proxy_pass的后端端口不通,ss -tlnp查一下后端到底有没有在听
2. 单独验证资源路径
$ curl -I http://example.com/app/assets/index-a1b2c3.js
如果这个是 200,而页面报 404,那一定是产物里的路径少了 /app/ 前缀(回到第二节)。
3. 看 error.log 里的真实磁盘路径
$ tail -f /var/log/nginx/error.log
open() "/data/sites/app/app/dashboard" failed (2: No such file or directory)
看到这种重复的 app/app,就是 alias 与 try_files 的冲突,直接对号入座。
改完配置,先 nginx -t 校验语法,通过再 nginx -s reload。直接 reload 一个语法错误的配置会让 Nginx 保持旧配置继续跑,看着"没生效",其实是被拒绝了——养成先 -t 的习惯能省很多困惑。
小结
子路径部署的复杂度,八成来自三件事:proxy_pass 的斜杠决定剥不剥前缀、产物的绝对路径决定了要不要改 base、root 与 alias 决定了 try_files 怎么写。把这三条理清,配置就是十来行的事。
如果条件允许,还有个更省事的方向:给每个站一个二级域名(app.example.com、docs.example.com),用 server_name 分流,路径永远从 / 开始,上面这一整类前缀问题直接消失。子路径方案的价值在于省域名、省证书,代价就是这几处容易错的地方——选之前先想清楚自己更在意哪一头。
- 点赞
- 收藏
- 关注作者
评论(0)