iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
佛心分享-SideProject30

為你自己蓋一座會複利的知識庫——WikiBrain系列 第 22 篇

Day 22 - 接上 Railway、Cloudflare、Resend 與 Google OAuth

  • 分享至 

  • xImage
  •  

前言

程式在平台上跑起來了,但網址還是平台給的那串,沒有網域、寄不出信、沒有 Google 登入。這一篇把四個外部服務一次接完:Railway 的設定、Cloudflare 的網域與 DNS、Resend 的寄信、Google Cloud 的 OAuth 用戶端。

所以這一篇寫成清單:點哪裡、填什麼、怎麼確認。判斷與理由之前我們已經講過了。

順序不能換

Railway 設定(先能跑)
  → Cloudflare 接網域(決定正式網址)
  → 回 Railway 改 APP_URL 並重新建置
  → Resend 寄信(要有網域才驗得過)
  → Google OAuth(redirect URI 要用正式網址)

之前我們已經講過,APP_URL 是建置參數,會被烤進預先渲染的頁面。所以網域必須在重新建置之前定案,順序反了就要再建置一次。

一、Railway

要填的變數

變數 值
DATABASE_URL 用平台的變數參照指向 Postgres 服務
APP_URL 先填臨時網址,網域好了再改
BETTER_AUTH_SECRET openssl rand -hex 32
KEY_ENCRYPTION_SECRET 另外一把 openssl rand -hex 32,不能跟上面同一把
NODE_ENV production

最後兩項的理由之前我們已經講過:兩把 secret 分開,而且正式環境沒設加密金鑰會直接拒絕啟動。

Railway 的 Variables 頁,五個變數都填好的樣子

這一頁的變數都是假值。Railway 預設也會把值遮成星號,但點一下就會顯示出來。

我踩過的四個地方

第一,健康檢查路徑要手動填。專案裡有設定檔寫著 healthcheck 路徑,但平台已經停用那個設定檔的支援,所以它被安靜地忽略。要在介面上手動填 /healthz。設定檔被忽略時不會報錯,部署照樣成功,只看設定檔會以為健康檢查已經開了。

這個欄位的位置與行為見 Railway 的 healthchecks 文件。

第二,注入的 PORT 不是你想的那個。Dockerfile 裡寫 ENV PORT=3000,但平台會注入自己的 PORT,實際是 8080。設定自訂網域時要填目標埠,填 3000 就會拿到 502「應用程式沒有回應」,而那個錯誤訊息完全不會告訴你埠錯了。

正確做法是伺服器一律讀 process.env.PORT,並且綁 0.0.0.0 而不是 localhost,綁 localhost 在容器裡等於誰都連不到。

目標埠的設定在 Railway 的 public networking 文件。

第三,未部署的變更會被丟掉。我在畫面上改了環境變數、選了機房,然後接著去新增資料庫服務。加完回來發現剛才的變更不見了,那些是暫存狀態,要先按部署才會生效。這件事我踩了三次才學會。

畫面上方顯示有未部署的變更,旁邊是 Deploy 按鈕

看到這條就先按下去,再做下一件事。

第四,資料庫也要選機房。每個服務各自設定,加資料庫的時候如果沒注意,app 在新加坡、資料庫在美國,每一次查詢多兩百毫秒。事後要改,Railway 會先警告:搬 volume 的那段時間資料庫會停機。所以加資料庫的當下就選對。

Postgres 的機房從預設的美東改成新加坡,畫面跳出搬移資料會停機的警告

二、Cloudflare:網域與 DNS

(網域本身是在 Cloudflare Registrar 買的。買網域的畫面他們自己會改,這裡只寫買完之後怎麼接到知識庫。)

在 apex 網域用 CNAME

想把不帶 www 的根網域指向託管平台,會碰到 DNS 標準不允許 apex 有 CNAME。傳統解法是用 A 記錄指到固定 IP,但平台通常不給你固定 IP。

現代的 DNS 服務有一個叫 CNAME flattening 的功能:你設定上填 CNAME,它在回答查詢時自己去解析、回傳 A 記錄。對外看起來完全合法,對你來說就像 CNAME 能用在 apex 上。

步驟

  1. Railway → Settings → Domains → 新增自訂網域,記下它給的 CNAME 目標與驗證用的 TXT
  2. Cloudflare DNS 加兩筆:CNAME @ 指向那個目標、TXT 完成驗證
  3. 再加 www 指向同一處,或設 301 轉到 apex
  4. 等憑證簽發,通常幾分鐘到二十分鐘
  5. curl -I https://你的網域/healthz 通了,才回 Railway 把 APP_URL 換成正式網址並重新建置

新增記錄的操作見 Cloudflare 的 DNS 記錄文件。

灰雲與橘雲

設定上每一列有一個開關:只做 DNS(灰雲),還是流量也走它(橘雲)。

灰雲只回答 DNS 查詢,流量直接打到你的伺服器。你的來源 IP 是公開的,前面也沒有快取或防火牆。

橘雲流量經過它,你拿到快取、防火牆規則、邊緣限流、DDoS 防護,來源 IP 也藏起來。

橘雲的限制在逾時:免費方案對「開始回應」通常有一百秒的限制。如果你有長時間的串流回應(MCP 就是),超過就會被切斷。所以切換之前要先確認:加密模式設成完整驗證、切換後實測長串流跟 OAuth 流程都還正常。

兩種狀態的差別與切換方式見 Cloudflare 的 proxy status 文件。

我目前還在灰雲,切換排在待辦清單上,因為它需要一次完整的驗證,不是點一個開關就好。

三、Resend:寄信的三筆記錄與 DMARC

發驗證信、重設密碼信需要一個寄信服務。Resend 要你加三筆 DNS 記錄,三筆都驗過,網域才能寄信:

  • DKIM:一筆 TXT,放公鑰,收件方用它驗證信件沒有被竄改
  • SPF:一筆 TXT,宣告哪些伺服器可以代表你的網域寄信
  • MX:讓退信回得來

SPF 與 MX 掛在 send 這個子網域底下,不會動到根網域的 MX,後面 hello@ 的轉信還能照常設。

三筆一個一個貼很容易打錯。請寄信服務給你一份 BIND 格式的 zone 檔,DNS 那邊用匯入功能一次貼完。

網域頁上有匯出 zone 檔的按鈕,位置見 Resend 的網域文件。這是這一節唯一的技巧,其餘都是等。

DKIM、MX、SPF 三筆記錄都顯示 verified

DMARC 不在這三筆裡,Resend 驗證網域時不檢查它,要自己另外加一筆 TXT:名稱 _dmarc,值從 v=DMARC1; p=none; 開始。p=none 只是觀察,SPF 或 DKIM 沒過的信,收件方照樣收下;確定信都寄得到、也都通過之後,再改成 quarantine 或 reject。Resend 建議的值還多一段 rua=mailto:…,指定彙整報告寄到哪個信箱,我沒有加。做法見 Resend 的 DMARC 文件。

驗證過了之後,回 Railway 填 RESEND_API_KEY 與 MAIL_FROM,部署。

沒設定寄信服務時驗證連結會印在伺服器日誌裡,本機開發跟自架的人不需要先申請一個寄信服務才能註冊第一個帳號。

四、Google OAuth 用戶端

  1. Google Cloud Console → 建立專案

  2. OAuth 同意畫面:填應用程式名稱、支援信箱、授權網域

  3. 憑證 → 建立憑證 → OAuth 用戶端 ID → 網頁應用程式

  4. 已授權的重新導向 URI 填:

    https://你的網域/api/auth/callback/google
    
  5. 拿到 client id 與 secret,填進 Railway 的 GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET,部署

這個欄位在三層選單底下,路徑見 Google 的 OAuth 2.0 網頁應用文件。

那個網址錯一個字元就會失敗,而錯誤訊息只會說 redirect_uri_mismatch,不會告訴你差在哪裡。

這裡我浪費了半小時。變數在平台上設好了,登入按鈕卻一直沒出現;程式的邏輯是兩個變數都有值才啟用,所以我開始懷疑自己的判斷式寫錯。原因跟 Railway 第三點一樣:變數還停在暫存狀態,沒有部署。管理介面上顯示著新的值,執行中的容器拿到的還是舊環境。

所以改完變數,我先確認部署紀錄多了一筆新的,再去測功能。管理介面上顯示的值不算數。

順手做完的兩件事

Search Console:用 TXT 記錄驗證網域所有權,然後提交 sitemap。

信箱轉址:hello@你的網域 轉到自己的信箱。用的是 Cloudflare Email Routing,免費方案就夠。說明頁跟法律頁上要有聯絡方式,而且它應該是網域信箱不是私人 Gmail。

全部接完之後怎麼確認

按下部署不等於上線成功。這份清單每改一次設定就跑一遍:

# 版本對不對:回傳的 commit 短碼要是剛推的那一個
curl -s https://你的網域/healthz

# 遷移有沒有套用:看部署日誌有沒有 "Applied migrations"

# MCP 端點還活著
curl -s -o /dev/null -w '%{http_code}\n' https://你的網域/mcp   # 預期 401

# OAuth metadata 的 issuer 是不是正式網址
curl -s https://你的網域/.well-known/oauth-authorization-server

# 公開頁面
curl -s -o /dev/null -w '%{http_code}\n' https://你的網域/robots.txt

/healthz 回傳的 commit 短碼,拿去跟本機的 git rev-parse --short HEAD 比,就知道線上跑的是不是剛推的那一版。少了這一項,就只能從功能有沒有變去猜。後來我把這幾條寫成一支測試,改任何設定之後跑一次。

截圖的安全提醒

這一篇的每一張圖都來自要登入的後台,畫面上很容易帶到不該外流的東西:來源主機名、資料庫連線字串、API key、帳號信箱、專案 ID。而截圖繞得過所有掃描原始碼的工具。

Railway 的三張圖來自一個拋棄式專案:新開、填假值、截完就刪,圖上本來就沒有敏感資料,不用一張一張塗黑,也就不會塗漏。Resend 那張用的是正式網域,只框記錄表格,網域名稱與帳號都在框外。

另一半的做法是不截。上面那些「這個設定在哪裡」的問題,我一律連到廠商自己的文件:他們的畫面永遠是最新的,而我的截圖三個月後就對不上。只有顯示「狀態」而不是「位置」的畫面才值得自己截:未部署的提示、記錄都驗過的樣子,那些是文件上沒有的。

小結

這一篇把跑在平台臨時網址上的程式接成正式服務:有自己的網域、寄得出驗證信、能用 Google 登入。各家後台怎麼點,官方文件都寫了;這一篇多交代的是先後。APP_URL 在建置時就寫進頁面,所以網域要最先定,寄信驗證和 OAuth 的 redirect URI 也都得等正式網域出來才填得了。


上一篇
Day 21 - 打包成 Docker 映像,部署上 Railway
系列文
為你自己蓋一座會複利的知識庫——WikiBrain 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言