Self-hosted Supabase 部署與遷移

自架 Supabase 真正的成本不是伺服器,是你收回來的那些責任:migration 不再由 CI 推、 備份變成自己的事、埠號撞在一起時會連到錯的環境而不報錯。這篇記遷移前該先確認的兩件事。

系列・Nuxt 4 全棧實戰筆記・第 7/12 篇

自架 Supabase 的理由通常只有幾個:資料必須留在自己的機器上、流量大到託管費用不划算、或者部署環境根本連不到外網。

如果不是這三種情況,不要自架——因為你換到的東西很有限,而收回來的責任很具體:migration 不再由 CI 推、備份沒有人幫你做、服務掛了要自己看 log。

你可能會想:那算一下成本再決定?可以,但別只算伺服器月費。真正該進到那張表的是你要付的注意力——備份有沒有真的在跑、磁碟還剩多少、上游改版之後容器還起不起得來。這些工作平常是零,出事那天是全部,而且沒有人會提醒你它們該做了。這篇是實際遷移過一次之後,覺得值得先講的幾件事。

遷移前先確認兩件事

這兩件決定了遷移是「改幾個環境變數」還是「重寫資料存取層」。

第一,URL 與金鑰有沒有寫死在程式碼裡。 全部走環境變數的話,應用程式端一行都不用改——這是實際的結果,不是理想狀況。有任何一處寫死,那一處就是遷移當天才會發現的地雷。

const supabaseUrl = process.env.NUXT_PUBLIC_SUPABASE_URL

第二,RLS policy 有沒有依賴 Supabase Auth 的身分。 如果 policy 裡有 auth.uid(),那它綁的是 Supabase 簽發的 JWT;自架之後這套仍然可以運作,但你等於把認證系統也一起搬了,使用者資料的遷移是另一個工程。

反過來說,如果認證本來就交給外部方案、policy 不靠 auth.uid() 認人,這一整塊就不存在——搬的只是資料庫本身。這是當初把認證拆出去時沒預料到的好處。

埠號一定要換掉

自架的服務跟本機開發用的 supabase start 會用到同一組預設埠號。沿用預設值的後果不是啟動失敗那麼單純——最糟的情況是連到了另一個環境而完全不知道。

所以自架端一律錯開:

KONG_HTTP_PORT=8001    # API gateway,預設 8000
STUDIO_PORT=3001       # 管理介面,預設 3000(也會撞 dev server)
POSTGRES_PORT=5433     # 資料庫,預設 5432

對外則走 Cloudflare Tunnel,不必在防火牆上開埠:

ingress:
  - hostname: supabase-api.example.com
    service: http://localhost:8001
  - hostname: supabase-studio.example.com
    service: http://localhost:3001
  - service: http_status:404

管理介面務必再加一層存取控制(Cloudflare Access 或 IP 限制)。它預設只靠一組密碼保護,而它能做的事等同資料庫的完整權限。

Migration 從 CI 自動變成手動

託管版的 CI 可以直接 supabase db push。自架之後資料庫在內網,CI 連不到——要嘛開 tunnel 給 CI,要嘛改成手動。

我們選手動,理由不是安全(tunnel 也可以做得很安全),而是自架環境的 migration 失敗時,沒有人會替你回滾。手動執行至少保證有人在看著輸出:

docker cp supabase/migrations/20240124000000_create_table.sql supabase-db:/tmp/
docker exec -it supabase-db psql -U postgres -d postgres \
  -v ON_ERROR_STOP=1 --single-transaction -f /tmp/20240124000000_create_table.sql
docker exec -it supabase-db psql -U postgres -d postgres -c "\dt public.*"

那兩個旗標不是可選的。psql 的預設行為是遇到錯誤繼續往下跑,而且整批跑完還是 exit 0—— 一支 migration 中間某句失敗,你會得到一個半套的 schema 加上一個看起來成功的終端輸出。 ON_ERROR_STOP=1 讓它第一個錯誤就停,--single-transaction 讓它要嘛全套要嘛全不套。

代價要說清楚:部署流程從此有一個人工步驟,而人工步驟會被忘記。所以本地那套「建 migration → 重建驗證 → lint → 產生型別」的紀律不能鬆——它是你在推上去之前唯一的驗證機會。

備份變成自己的事

託管版的備份是預設就有的,自架之後它變成一個你必須主動建立、而且必須驗證過還原流程的東西。

docker exec supabase-db pg_dump -U postgres -d postgres \
  --schema=public --no-owner --no-privileges | gzip > "backup_$(date +%Y%m%d).sql.gz"

排除 auth / storage / realtime 這些由 Supabase 自己管理的 schema,還原時比較不會撞版本差異。然後排進 crontab、設保留天數——但真正重要的是至少完整還原過一次。沒有還原過的備份不算備份,只是一堆檔案。

一個花了時間才找到的坑

MCP endpoint 連不上,服務全部正常、log 沒有錯誤。

原因是 API gateway 的路由設定檔(volumes/api/kong.yml)裡沒有那條路由。gateway 對沒有登記的路徑就是回 404——它沒有做錯任何事,只是它不知道那條路徑該轉給誰。

這類問題的共同形狀是:自架把「有人幫你設定好」變成「設定檔裡沒有就是沒有」。托管版看不見的那些設定,自架之後全部變成你的檔案,包含你不知道它存在的那幾個。