一个域名两个后端

三个容器都起来了,本机直连端口全通。可浏览器打开前端域名,首页能出来,一登录、一进列表,控制台一排 404。所有 api 都 404。

先把背景说一下。用户不直接连你的容器,而是先连到面板的 Web 层,也就是一个 OpenResty 容器,它直接占着宿主机的 80 和 443,由它按规则把请求转发给后面的业务容器。这层转发规则,就是后面反复出现的 location。

三个容器是这样分的。H5 静态站在 18091,Go 接口在 18080,Python 后台在 18081。同机上还跑着 6 个别人的线上站点。

起手先各自连通一遍。

bash 复制代码
ss -ltnp | grep -E ':18080|:18081|:18091'

curl -s http://127.0.0.1:18080/api/v1/products | head -c 120   # Go 后端 → JSON
curl -sI http://127.0.0.1:18091/               | head -1       # H5 静态 → 200
curl -sI http://127.0.0.1:18081/auth/login     | head -1       # 后台 → 200/302

三条全通。然后去面板建站:H5 站点填「代理地址 http://127.0.0.1:18091」,接口站点填 18080,后台站点填 18081。浏览器再打前端域名,首页出来了,一登录、一进列表,控制台一排 404。

第一反应通常是「后端挂了」。可后端三十秒前还好好的。

问题不在后端

关键在于,前端容器是个纯静态 nginx。

它的镜像构建文件里只有一行实质性内容,COPY dist,把构建产物拷进 nginx 的默认静态目录。它会的只有一件事:把 dist/ 里的 js、css、html 吐出去。它没有任何 /api/ 路由,也不会把请求转发给别人。

而建站的时候,我当时只告诉了面板一件事:这个站点所有请求都送到 18091。面板于是只生成一条规则,location / 指向 18091。

于是浏览器里那句 fetch('/api/v1/products') 的真实旅程是这样的。

bash 复制代码
浏览器 → 面板 OpenResty(80/443)→ location / → 127.0.0.1:18091(静态容器)
                                                └─ 这里没有 /api/ 这个路径 → 404

404 在这里不代表接口报错。它的意思是,这个路径在这台服务器上不存在。

前端代码没错,Go 后端也没坏。请求被送错了门。

三条命令把责任钉在面板层

不要猜,打三条请求,把「哪一层坏了」变成一道选择题。

bash 复制代码
# ① 走域名(经面板反代)
printf "经面板 /         → "; curl -s -o /dev/null -w "%{http_code}\n" -H 'Host: h5.example.com' http://127.0.0.1/
printf "经面板 /api/...  → "; curl -s -o /dev/null -w "%{http_code}\n" -H 'Host: h5.example.com' http://127.0.0.1/api/v1/products
# 实测:200 / 404

# ② 绕过面板,直连静态容器
printf "直连 18091 /api/ → "; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:18091/api/v1/products
# 实测:404

# ③ 绕过面板,直连 Go 后端
printf "直连 18080 /api/ → "; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:18080/api/v1/products
# 实测:200

怎么读这三条。第一条走域名、经面板反代,首页拿到 200,说明面板到静态容器这一段是通的,面板本身没坏。第二条绕过面板直连 18091,也是 404,说明这个 404 是静态容器自己答的,不是面板答的。第三条绕过面板直连 18080,拿到 200,说明 Go 后端完全健康,接口逻辑没问题。三条放一起,结论只有一个:后端没病,缺的是一条把 /api/ 单独指过去的规则。

顺带一句,这套「经代理一次加绕开代理直连上游一次」的对比,是所有反代故障的通用定位手法。两次结果一致,问题在上游;两次结果不一致,问题在代理层。

该补的是第二条 location

在该站点的配置文件里,location / 之前插入这一段。

nginx 复制代码
    # 静态走 18091;API 走 Go 18080(缺这段,前端所有接口 404)
    location /api/ {
        proxy_pass http://127.0.0.1:18080;
        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 X-Forwarded-Proto $scheme;
        proxy_read_timeout 30s;
    }

后面那几个 proxy_set_header 不是装饰,漏掉任何一个都会埋雷。依据 nginx 官方的 ngx_http_proxy_module 文档,转发请求默认用 HTTP/1.0,Host 默认取 proxy_pass 里的地址。

proxy_http_version 1.1 不写,就按 HTTP/1.0 转发,长连接和后续升级能力丢掉。Host host不写,后端收到的是127.0.0.1,按域名判断路由或者拼接回调地址的服务会出错。X−Real−IP和X−Forwarded−For不写,后端只看到代理的地址,日志、限流、风控全部失真。X−Forwarded−Protohost 不写,后端收到的是 127.0.0.1,按域名判断路由或者拼接回调地址的服务会出错。X-Real-IP 和 X-Forwarded-For 不写,后端只看到代理的地址,日志、限流、风控全部失真。X-Forwarded-Proto host不写,后端收到的是127.0.0.1,按域名判断路由或者拼接回调地址的服务会出错。X−Real−IP和X−Forwarded−For不写,后端只看到代理的地址,日志、限流、风控全部失真。X−Forwarded−Protoscheme 不写,后端不知道外面是 https,生成的绝对链接会回落成 http。

改完保存,面板会自动 reload。重跑取证那三条里的第一条,/api/v1/products 应该从 404 变成 200 并返回 JSON。

三个域名得建三个独立站点

这一步我踩过了。面板建站的时候有个「其他域名」字段,看起来很贴心,把前端域名、接口域名、后台域名全填进去,一个站点搞定。

不行。一个站点就是一份反代配置。三个域名共用一份配置,前端域名就拿不到「静态容器优先、接口走后端」的分流,接口域名也拿不到「全部走后端」的规则,三个域名的路径规则被强行统一了。

正确做法是建三个独立站点,每个站点的主域名只填自己那一个,其他域名留空。

H5 站点的主域名填 h5.example.com,代理地址 http://127.0.0.1:18091,必须补上 /api/ 指到 18080 这条规则。接口站点的主域名填 api.example.com,代理地址 http://127.0.0.1:18080,不需要额外改动。后台站点的主域名填 admin.example.com,代理地址 http://127.0.0.1:18081,建议加上访问来源限制。

还有一层坑来自面板默认生成的配置形态。先确认你的站点主 location 长什么样。

bash 复制代码
OR=$(docker ps --format '{{.Names}}' | grep -i openresty | head -1)
docker exec "$OR" nginx -T 2>&1 | grep -E "^\s*location" | sort -u

如果看到的是 location ^~ / {,后面就必须用精确匹配 location = /路径。如果看到的是 location / {,就可以用正则。

location 匹配指的是 nginx 决定「这个 URL 交给哪段配置处理」的规则。= 精确匹配优先级最高,命中就停止搜索。前缀匹配次之,其中带 ^~ 的前缀一旦命中,会跳过后面的正则阶段。

面板生成的就是 location ^~ /。它匹配所有路径,而且 ^~ 的优先级高于正则,任何靠正则写的 location 都会先被它吃掉,请求直接反代给业务容器,然后 404。

所以有一条不用动脑的规则:凡是要在面板层就被接住的具体路径,比如第三方校验文件、探针文件,一律写成精确匹配。

nginx 复制代码
    location = /a1b2c3d4e5f60718293a4b5c6d7e8f90.txt {
        default_type text/plain;
        return 200 '这里是校验内容';
    }

= 与路径之间留空格。= 一旦命中就停止搜索,不受任何其它 location 影响。这也是唯一不需要先去查主 location 写法的写法。

相关推荐
技术小结-李爽2 小时前
【工具】怎么在终端中启动springboot项目?
java·spring boot·后端·maven
乌暮2 小时前
从 `println` 到生产级日志:Spring Boot 日志体系完全指南
java·开发语言·jvm·spring boot·后端·学习·面试
鶴哥只手遮天2 小时前
Spring Boot 4.1.1 深度源码解析-第 29 章 · 测试体系:`@SpringBootTest`、切片测试、Testcontainers
java·后端·开源
yunwei372 小时前
eBPF 实例教程:BPF 动态指针处理可变长度数据
linux·后端·性能优化
tltwuyulw2 小时前
Java的函数式编程(五)
java·后端
王中阳Go2 小时前
第一单交付完,我等 3 个月没复购:不是交付差,是这 3 件事没做
人工智能·后端·面试
知守观3 小时前
Maven 依赖仲裁实战:pom 里写的 2.0,为什么打出的 WAR 包是 1.5?
java·后端·maven
打工仔折腾 AI3 小时前
260+异构同步链路实战:如何用语义化自动翻译破解CDC DDL同步痛点
人工智能·后端·python·自然语言处理·性能优化·langchain·机器翻译