踩坑记录
今天到底踩了哪些坑?
先把坑摊开说清楚,再一一拆解它们背后的“第一性原理”。
今天踩到的核心坑有这些:
-
端口只能被一个进程监听
-
OpenResty 抢着用
80/443,Dokploy(Traefik)也要用80/443,结果就是——谁也干不好。 -
甚至我以为已经改完端口,其实
default.conf还在偷偷listen 80。 -
字符串不完全匹配导致 sed 没生效
-
配置里有的是
listen 80;,有的是listen 80 ;。 -
我用
sed 's/listen 80;/listen 8080;/g',结果一大半没改到,还以为自己改完了。 -
看
ss结果没搞清“进程有没有在跑” -
有一次
ss | grep ':8080'没任何输出,我还在纠结“为什么 8080 不显示”,其实是:OpenResty 压根没启动。 -
Traefik + OpenResty 双重 SSL + HSTS
-
以前:OpenResty 直接在
443做 HTTPS,证书是 1Panel 自动签的,浏览器记住了 HSTS。 -
现在:Traefik 在
443做 HTTPS,OpenResty 还在8443做 HTTPS,于是出现“双层 SSL + HSTS”地狱,浏览器直接报:你的连接不是私密连接 /
ERR_CERT_AUTHORITY_INVALID -
搞混“应用监听端口”和“反向代理转发端口”
-
容器里程序在听 80,我却告诉 Dokploy:这个服务在 3000。
-
结果 Traefik 一直去连
:3000,当然只能得到 502。 -
搞混“容器里的世界”和“宿主机的世界”
-
在宿主机上访问
http://host.docker.internal:4000,发现不通,以为服务挂了。 -
实际上
host.docker.internal这个名字只有容器里认识,宿主机根本不知道这是谁。 -
路径反代
/apivs 子域名api.xxx.com的复杂度差异 -
以前习惯
todo.626909.xyz/api这种方式; - 到了 Traefik,要玩 PathPrefix、StripPrefix、CORS,心智负担大很多;
- 最后我干脆接受“一个服务一个子域名”这种云原生思路:
api.todo.626909.xyz。
这些坑看起来很杂,其实都绕不过几个核心问题:
- 一个 IP 的 80/443,究竟谁说了算?
- HTTPS / 证书到底是怎么工作的?
- 反向代理在整个请求链路里扮演什么角色?
- 浏览器的同源策略、跨域是怎么回事?
- 前端、后端、反向代理三者的边界到底在哪?
下面我就从「人输入一个网址开始」,把整个链路按时间顺序拆开。
从第一性原理说:我输入网址以后,发生了什么?
假设我在浏览器里输入:
https://todo.626909.xyz
大概发生了这些事(简化版):
1. 浏览器先问:这个域名对应哪个 IP?
- 浏览器查本地缓存、系统的 DNS 缓存;
- 找不到就问 DNS 服务器(运营商、公共 DNS 等):
“
todo.626909.xyz的 IP 是多少?”
- DNS 回复,比如:
203.0.113.10
于是浏览器得到了:
目标地址:203.0.113.10:443
协议:https
2. 决定用 HTTP 还是 HTTPS(甚至 HSTS 直接升级)
- 如果是输入
http://,正常会走 80 端口; - 如果是
https://,就走 443 端口; - 如果浏览器以前访问过这个域名,而且对方设置了
Strict-Transport-Security(HSTS),即使我输入http://,浏览器也会 强行升级成 HTTPS,只信任 443,而且必须是合法证书。
我今天踩的 HSTS 坑就是: 浏览器记住了这个域名曾经是“必须 HTTPS”的。 后来我换了一套 TLS 终止方案,但证书不对,浏览器直接不让我访问。
3. TCP 连接 & TLS 握手(HTTPS 部分)
浏览器对 203.0.113.10:443 发起 TCP 连接:
- 三次握手建立 TCP:SYN → SYN/ACK → ACK
-
开始 TLS(SSL)握手:
-
浏览器发
ClientHello(带上自己支持的加密方式、SNI:todo.626909.xyz) - 服务器返回一个 证书链(包括域名证书 + 中间 CA)
-
浏览器检查:
- 证书里的域名是否匹配当前请求域名;
- 证书是否在有效期内;
- 证书是否由一个受信任的 CA 签发;
- 如果任何一步失败,就会看到今天那个错误页面。
这一步非常关键: 谁监听 443,谁就负责给出证书。 只能有一个进程监听 443,也只能有一个地方终止 TLS。
- 以前是 OpenResty → 它给浏览器返回由 1Panel + acme.sh 签的证书;
- 现在是 Traefik → 它要么用自己的自签证书,要么用 Let’s Encrypt 签的证书;
- 如果我没给 Traefik配置好 Let’s Encrypt,它就会拿一个浏览器不信任的证书上来,HSTS 就炸了。
4. 通过了 TLS,才开始真的发 HTTP 请求
握手通过之后,才会发 HTTP 报文:
GET / HTTP/1.1
Host: todo.626909.xyz
User-Agent: ...
Accept: text/html, ...
...
到这里,浏览器做完了“门口的事”,请求已经送到服务器的 入口 了。 下一步是:服务器内部怎么处理这个请求?
服务器内部:为什么我需要反向代理?
有一个最基本的“物理事实”:
一个端口(比如 80、443),同一时间只能被一个进程监听。
但我又想在一台机器上跑很多服务:
wenzhixuan.comtodo.626909.xyznewapi.626909.xyzchat.townboats.me- 以及 Dokploy 自己的后台等……
难道每个服务都用一个新的端口?比如:
todo.626909.xyz:8001wenzhixuan.com:8002newapi.626909.xyz:8003
从用户体验和运维角度,都灾难级别。
于是就有了反向代理(Reverse Proxy)这个角色:
反向代理 = 大门口的总前台
- 它绑定在 80/443 上;
- 浏览器永远只看到“它”;
-
它收到请求后,根据:
-
域名(
Host) - 路径(
/api/、/static/) - 甚至 HTTP 头 来决定:
“这个请求应该转发给哪一个后端服务?”
从第一性原理看,反向代理解决的是一个“一进多出”的问题:
- 进:统一入口(一个 IP:80/443)
- 出:N 个后端服务(各种端口、各种语言、各种框架)
我今天遇到的“OpenResty vs Traefik 抢端口”本质就是:
一个入口,只能有一个总前台。 不能同时有两个“反向代理”抢 80/443。
以前的总前台是 OpenResty,现在我把总前台换成了 Traefik(Dokploy)。
那跨域(CORS)又是怎么冒出来的?
当浏览器渲染前端页面时(比如 React/Vite 构建产物),页面里会发请求:
fetch('https://api.todo.626909.xyz/todos')
浏览器会检查:这个请求是不是“跨域”。
什么叫“同源 / 跨域”?
浏览器定义“源(origin)”是这样三件事的组合:
协议(scheme) + 域名(host) + 端口(port)
- 如果这三项都一致,就是同源;
- 有任何一项不同,就是跨源 / 跨域。
举例:
| 页面地址 | 请求地址 | 是否同源 |
|---|---|---|
| https://todo.626909.xyz | https://todo.626909.xyz/api/todos | ✅ 同源 |
| https://todo.626909.xyz | https://api.todo.626909.xyz/todos | ❌ 跨域(域名不同) |
| https://todo.626909.xyz | http://todo.626909.xyz/api/todos | ❌ 跨域(协议不同) |
| https://todo.626909.xyz | https://todo.626909.xyz:8443/api | ❌ 跨域(端口不同) |
浏览器有个 同源策略(Same-Origin Policy):
默认情况下,前端 JS 不允许随便访问其它源的数据, 以防一个恶意网站在你登录银行时偷偷发请求。
所以:
- 如果前端页面和 API 不同源,就叫“跨域请求”;
- 要么后端显式添加
Access-Control-Allow-Origin等 CORS 头; - 要么通过反向代理把它们“伪装成同源”。
这跟我今天的操作有什么关系?
以前你是:
页面: https://todo.626909.xyz
API: https://todo.626909.xyz/api/...
- 页面和 API 完全同源(协议 + 域名 + 端口一致);
- API 的路径由 OpenResty 反代到后端
8000。
迁移以后:
- 你打算用
https://api.todo.626909.xyz作为 API 域名; - 页面还是
https://todo.626909.xyz; - 这两者是不同源;
-
相应地:
-
要么 API 返回允许跨域的 CORS 头;
- 要么在 Traefik / Dokploy 里继续做统一代理。
这就是为什么很多框架教程一上来就建议:
- 开发环境用代理(devServer proxy);
- 生产环境要么用 Nginx 做统一入口,要么给 API 设置好 CORS。
前端、后端、反代;这三者到底怎么分工?
可以简单理解成:
1. 前端(浏览器运行的那一部分)
- 本质是一堆 HTML / JS / CSS;
- 由某个静态服务器、对象存储或 CDN 提供;
-
在浏览器里运行后,会发起:
-
获取页面数据的请求(API)
- 提交表单、登录请求
- WebSocket 连接等
它看到的世界只有 URL,根本不知道“后面有多少服务”。
2. 后端(真正跑业务逻辑的那一部分)
- 可能是 Node / Python / Go / Java;
- 监听某个端口(比如 4000 / 8000);
- 不关心 HTTPS(很多时候直接跑 HTTP);
- 不关心浏览器安全策略(只是返回 JSON / HTML)。
3. 反向代理(Nginx / OpenResty / Traefik / Caddy 等)
-
唯一知道全局情况的那一个:
-
哪些域名归我管;
- 哪些域名对应哪些服务;
- 哪些要 HTTP→HTTPS 跳转;
- 哪些要去容器,哪些要去宿主机;
- 决定给谁做 SSL 终止;
- 决定要不要加 CORS 头;
- 决定要不要做路径改写,比如
/api→/。
今天这一整天,其实就是在做一件事:
把“总前台”从 OpenResty 换成 Dokploy(Traefik), 同时把原来散落在系统里的服务,一点点挂到新前台后面。
在这个过程中,所有坑其实都可以用一句话解释:
- 我忘了/没完全搞清楚: 浏览器只看到 IP:80/443; 那后面的那一堆东西,是怎么通过这个入口被串起来的。
回头看那几个坑:现在就很好解释了
-
端口冲突: 一个端口只能被一个进程监听。 80/443 一定要从 OpenResty 手里收回来,才能交给 Traefik。
-
sed 替换没改全: 是字符串匹配问题,导致 default.conf 还在监听 80,让我以为已经改完了。
-
ss看不到 8080/8443: 是因为 OpenResty 根本没启动。ss只显示“当前在监听的端口”,不是“你写在配置文件里的端口”。 -
双重 SSL + HSTS 爆炸:
-
浏览器记住这个域名必须 HTTPS(HSTS),而且证书必须可信;
- 我把 HTTPS 从 OpenResty 挪到 Traefik,中途证书没跟上;
-
OpenResty 又在后面搞一层 SSL,整个链路变得非常诡异。
-
应用端口 vs 代理端口搞混:
-
容器里服务听 80;
- 我跟 Traefik 说“它在 3000”;
-
本质上是搞混「程序在哪听」和「反向代理往哪转」。
-
host.docker.internal 在哪生效:
-
宿主机的
/etc/hosts不认识这个名字; -
它是给容器内部做“桥接宿主机”的一个特殊域名。
-
路径反代 vs 子域名:
-
路径反代看起来简单:
/api→ 后端; - 真正在 Traefik / CORS / 前端里搞清楚,就会变成一堆规则;
- 子域名
api.todo.626909.xyz反而更清晰,各司其职。
最后的一点感受
今天一圈折腾下来, 看似是在「换个面板、用新工具」, 其实是把这些基础概念都重新推倒重来:
- 一个 IP 的 80/443,到底是谁的?
- TLS 证书应该在哪一层终止?
- 浏览器如何判定“这个响应安全不安全”?
- 前端看到的,永远只是一个 URL 字符串;
- 真正把请求送到对应服务的,是那一个“总前台”——反向代理。
理解了这一整条链路,再回头看 Dokploy / Traefik / OpenResty,就不再是“某某神秘黑盒”,而是:
DNS → TCP/TLS → 统一入口(反代) → 后端服务 → 再返回给浏览器。