三个容器都起来了,本机直连端口全通。可浏览器打开前端域名,首页能出来,一登录、一进列表,控制台一排 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−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 写法的写法。