Nginx 反向代理:80 端口子路径部署静态站的配置要点

举报
yd_282015185 发表于 2026/10/27 12:01:02 2026/10/27
【摘要】 一台 ECS 只有一个 80 端口,用子路径放多个站点时,坑高度集中:proxy_pass 末尾的斜杠决定剥不剥前缀,root 与 alias 一个是拼接一个是替换,产物里的绝对路径会让样式全丢,try_files 碰上 alias 还会拼出重复目录。逐条讲清规则,附可直接用的完整配置与三步排查法。

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 分流,路径永远从 / 开始,上面这一整类前缀问题直接消失。子路径方案的价值在于省域名、省证书,代价就是这几处容易错的地方——选之前先想清楚自己更在意哪一头。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。