今天到底踩了哪些坑?

先把坑摊开说清楚,再一一拆解它们背后的“第一性原理”。

今天踩到的核心坑有这些:

  1. 端口只能被一个进程监听

  2. OpenResty 抢着用 80/443,Dokploy(Traefik)也要用 80/443,结果就是——谁也干不好。

  3. 甚至我以为已经改完端口,其实 default.conf 还在偷偷 listen 80

  4. 字符串不完全匹配导致 sed 没生效

  5. 配置里有的是 listen 80;,有的是 listen 80 ;

  6. 我用 sed 's/listen 80;/listen 8080;/g',结果一大半没改到,还以为自己改完了。

  7. ss 结果没搞清“进程有没有在跑”

  8. 有一次 ss | grep ':8080' 没任何输出,我还在纠结“为什么 8080 不显示”,其实是:OpenResty 压根没启动。

  9. Traefik + OpenResty 双重 SSL + HSTS

  10. 以前:OpenResty 直接在 443 做 HTTPS,证书是 1Panel 自动签的,浏览器记住了 HSTS。

  11. 现在:Traefik 在 443 做 HTTPS,OpenResty 还在 8443 做 HTTPS,于是出现“双层 SSL + HSTS”地狱,浏览器直接报:

    你的连接不是私密连接 / ERR_CERT_AUTHORITY_INVALID

  12. 搞混“应用监听端口”和“反向代理转发端口”

  13. 容器里程序在听 80,我却告诉 Dokploy:这个服务在 3000。

  14. 结果 Traefik 一直去连 :3000,当然只能得到 502。

  15. 搞混“容器里的世界”和“宿主机的世界”

  16. 在宿主机上访问 http://host.docker.internal:4000,发现不通,以为服务挂了。

  17. 实际上 host.docker.internal 这个名字只有容器里认识,宿主机根本不知道这是谁。

  18. 路径反代 /api vs 子域名 api.xxx.com 的复杂度差异

  19. 以前习惯 todo.626909.xyz/api 这种方式;

  20. 到了 Traefik,要玩 PathPrefix、StripPrefix、CORS,心智负担大很多;
  21. 最后我干脆接受“一个服务一个子域名”这种云原生思路: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 连接:

  1. 三次握手建立 TCP:SYN → SYN/ACK → ACK
  2. 开始 TLS(SSL)握手:

  3. 浏览器发 ClientHello(带上自己支持的加密方式、SNI:todo.626909.xyz

  4. 服务器返回一个 证书链(包括域名证书 + 中间 CA)
  5. 浏览器检查:

    • 证书里的域名是否匹配当前请求域名;
    • 证书是否在有效期内;
    • 证书是否由一个受信任的 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.com
  • todo.626909.xyz
  • newapi.626909.xyz
  • chat.townboats.me
  • 以及 Dokploy 自己的后台等……

难道每个服务都用一个新的端口?比如:

  • todo.626909.xyz:8001
  • wenzhixuan.com:8002
  • newapi.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 → 统一入口(反代) → 后端服务 → 再返回给浏览器。