專案筆記

Cloudflare Tunnel 路徑分流六個後端:cloudflared 不會幫你去掉前綴

2026-09-29 #.NET#Python#Cloudflare Tunnel#Docker#Java

Cloudflare Tunnel 讓主機不開 80/443 也能對外服務,而且可以用路徑把同一個網址分給六個後端。但 cloudflared 只負責「送到哪裡」,不會去掉路徑前綴:送進 .NET 的請求仍是 /dotnet/items。結果六個後端都得自己處理前綴,而且帶不帶前綴都要能動,六種框架的做法差很多。這是連載的第二篇,接續上一篇的 Pull-based CD 架構。

Cloudflare Tunnel 是什麼

主機上跑一個 cloudflared,它主動往外連到 Cloudflare,建立一條通道。外面的人連 https://<後端網址>,Cloudflare 再透過這條通道把請求送進主機。好處:

  • 主機不用開 80/443,也不用固定 IP
  • HTTPS 憑證由 Cloudflare 處理
  • 可以搭配防火牆把所有對內連線擋掉

這和上一篇的 self-hosted runner 是同一個思路:連線都由主機往外發起,主機本身不接受任何對內連線。

用路徑分流

六個後端共用同一個網址,用路徑前綴區分:

對外路徑 轉到 後端
https://<後端網址>/node/... 127.0.0.1:3000 Node
https://<後端網址>/dotnet/... 127.0.0.1:3001 .NET
https://<後端網址>/php/... 127.0.0.1:3002 PHP
https://<後端網址>/py/... 127.0.0.1:3003 Python
https://<後端網址>/go/... 127.0.0.1:3004 Go
https://<後端網址>/java/... 127.0.0.1:3005 Java
其他路徑 404

另一個選擇是每個後端一個子網域,程式不用改、隔離也更好。這次想練習路徑分流,所以選了共用網址,代價就是下面的前綴問題。

我寫了一支腳本把設定自動化:

1
2
3
4
5
6
7
./scripts/vm/setup-tunnel.sh \
<後端網址>/node=http://127.0.0.1:3000 \
<後端網址>/dotnet=http://127.0.0.1:3001 \
<後端網址>/php=http://127.0.0.1:3002 \
<後端網址>/py=http://127.0.0.1:3003 \
<後端網址>/go=http://127.0.0.1:3004 \
<後端網址>/java=http://127.0.0.1:3005

腳本依序:安裝 cloudflared → tunnel login(用瀏覽器授權網域)→ 建立 tunnel → 把憑證複製到 /etc/cloudflared/(root、600)→ 寫入 config.yml → 建 DNS CNAME → 安裝 systemd 服務 → 驗證每條路由的 /health。可以重複執行。

產生的 config.yml 大致如下:

1
2
3
4
5
6
7
8
9
10
11
tunnel: <tunnel-id>
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: <後端網址>
path: ^/node(/.*)?$
service: http://127.0.0.1:3000
- hostname: <後端網址>
path: ^/dotnet(/.*)?$
service: http://127.0.0.1:3001
# ...其餘四個
- service: http_status:404

path 用 ^/node(/.*)?$,才不會誤中 /nodex 這種路徑。

最大的坑:cloudflared 不會去掉前綴

請求 https://<後端網址>/dotnet/items 送到 .NET 時,路徑還是 /dotnet/items,不是 /items。所以每個後端都要自己處理前綴。做法是在 compose 設環境變數 PATH_BASE=/dotnet,程式啟動時讀它。

而且沒帶前綴的請求也要照常處理:本機測試、容器 healthcheck、CD 的驗證都是直接打 127.0.0.1:<port>/health,不會帶前綴。

還有一個容易漏掉的地方:POST /items 成功時回 201 加上 Location: /items/42,經過 tunnel 時要變成 Location: /dotnet/items/42,不然瀏覽器會跟到錯的路徑。smoke test 會檢查這一點。

各語言的做法差很多,這裡先列結論,下一篇再細講各框架的脾氣:

語言 做法 坑
Node 自己比對路徑前綴 無
.NET app.UsePathBase(pathBase) 後面必須明確呼叫 app.UseRouting(),否則 Minimal API 比對的是還帶前綴的路徑,全部 404
PHP 自己在 index.php 去掉前綴 無
Python 設定 ASGI 的 root_path path 要保留完整路徑,只設 root_path,Starlette 比對時會自己去掉
Go 自己比對路徑前綴 無
Java 用 filter 把前綴包成 context path 不能用 server.servlet.context-path,設了之後沒帶前綴的請求會 404

有趣的是,「無」坑的都是沒用框架、自己比對字串的版本。框架越想幫你處理路徑,越需要知道它在背後做了什麼。

防火牆與維運

tunnel 確認可以用了,才開防火牆:

1
sudo ./scripts/vm/setup-firewall.sh

它會設定 ufw default deny incoming,並在 ufw enable 之前先 ufw allow OpenSSH。如果先啟用防火牆、之後才放行 SSH,執行當下就可能把自己的 SSH 連線鎖在外面。腳本也提供 --no-ssh 選項連 SSH 都關掉,但偵測到目前有 SSH 連線時會直接中止。runner 和 cloudflared 都只用對外連線,不受防火牆影響。

另外一支維運腳本負責三件事:每週清理 7 天前未使用的 Docker image(不動 volume,資料庫資料安全)、建立 swap、開啟 Ubuntu 自動安全更新。

這一篇踩到的坑

  • 每次都要列出「全部」路由:腳本依參數整份重寫 config,只列一個會把其他路由刪掉。
  • 腳本是在主機上的 clone 執行的,不會隨 CD 更新:曾經在 PR 合併前就用舊版腳本跑新的參數格式,config 沒改成功,/dotnet/... 被送到 Node 而回 404。之後規定:更新 tunnel 前先在主機上 git pull,然後用 sudo cat /etc/cloudflared/config.yml 確認每條 path: 規則都在。
  • 改路由時,先部署程式、再更新 tunnel:反過來的話,tunnel 已經把新路徑導向還沒部署的服務,對外會暫時 404/502。
  • cloudflared 的憑證位置:tunnel login 以一般使用者執行,憑證在 ~/.cloudflared/;但 systemd 服務以 root 執行讀不到,所以要複製到 /etc/cloudflared/。
  • Cloudflare 免費憑證只涵蓋一層子網域:a.<你的網域> 可以,api.dotnet.<你的網域> 不行。如果改走子網域分流,命名要避開多層。
  • Dashboard 裡的舊路由:Tunnel 列表曾出現一條「非作用中」的舊 hostname,是之前試驗留下的,確認沒在用之後刪除即可。

小結

Tunnel 讓主機不開 port 也能對外服務,但路徑分流的代價是每個後端都要處理前綴。下一篇講怎麼讓六種語言寫出的 API 行為完全一致。

系列文章

  1. 架構與 Pull-based CD
  2. 本篇:部署主機與 Cloudflare Tunnel
  3. 六種語言、同一套 API(9/30 刊出)
  4. CI/CD 細節與部署加速(10/1 刊出)
  5. 前端:Cloudflare Workers 代轉(10/2 刊出)
  6. 公開與保護:Cloudflare Access 排錯全紀錄(10/3 刊出)

完整程式碼、ADR 與專案的 CLAUDE.md 都在 murmur-wu/DevBuildSample。

Creative Commons 姓名標示 非商業性

本文採用 CC BY-NC 4.0 授權

歡迎轉載與引用,請標明出處(Cloudflare Tunnel 路徑分流六個後端:cloudflared 不會幫你去掉前綴 — mur mur);禁止用於商業用途。

商業使用或合作提案,歡迎來信洽談:[email protected]

留言
分享

留言