自架部署(Docker Compose):把整套 Supabase 架在自己的伺服器上 | Supabase 完整教學
前面整個 CLI 段,我們都站在 Supabase 雲端這一側:用遷移管理 schema、給每個 PR 開預覽環境、把部署交給 CI/CD。這一篇,我們要換一條路——用 Docker Compose 把整套 Supabase 自己架在自己的伺服器上,也就是所謂的自架(self-hosting)。你會認識這套堆疊由哪些服務組成(db、Kong、Auth、PostgREST、Realtime、Storage、Studio、postgres-meta)、學會用
generate-keys生成JWT_SECRET、ANON_KEY、SERVICE_ROLE_KEY,設好.env裡的POSTGRES_PASSWORD與SITE_URL,再用反向代理處理 HTTPS,一路docker compose up到成功登入 Studio。這是自架段(SB-5)的第一篇。
前言
先一句話定義本篇要做的事:自架 Supabase,就是把原本 Supabase 雲端幫你托管的整套後端服務,用 Docker Compose 在你自己的伺服器上原封不動地跑起來。 你拿到的不是一個黑盒子,而是一份 docker-compose.yml 和一份 .env——十幾個容器化服務,各自負責資料庫、認證、API、即時、儲存與管理介面,由你自己啟動、自己維護。
用一個類比理解雲端與自架的差別。如果說 Supabase 雲端像「住旅館」——你只管入住,水電、清潔、保全、早餐都有人打理;那自架就是「自己買房」——你擁有完整的產權與鑰匙,想怎麼裝修都行,但水管漏了、電表跳了、門鎖壞了,全部得自己修。 自架換來的是資料主權與完全掌控,代價是你得自己扛起備份、升級、監控、故障排查這些原本雲端替你扛的維運工作。
也因此,自架不是「更進階、所以更好」的選項,而是取捨。適合自架的情境很明確:法規要求資料留在境內、air-gapped 內網環境、需要自訂 PostgreSQL 擴充、或資料量大到雲端方案不敷使用且你有 DevOps 人力。若你只是想省錢卻沒有維運人力,自架的「隱藏成本」——工程師的時間——往往比雲端月費更貴。
本篇你會學到:
- 自架堆疊的全貌:Docker Compose 由哪些服務組成、各自負責什麼、
Kong為何是唯一對外入口 - 金鑰與
.env關鍵設定:用generate-keys.sh生成JWT_SECRET/ANON_KEY/SERVICE_ROLE_KEY,設好POSTGRES_PASSWORD、SITE_URL - 從零啟動到登入 Studio:
git clone官方 docker 目錄 → 換金鑰 →docker compose up→ 打開管理介面 - 反向代理與 HTTPS:為何不能直接對外暴露 Kong,如何用 Caddy/Nginx 處理 TLS
本篇聚焦「把自架跑起來」這件事——架構、金鑰、
.env、初次啟動、反向代理。備份、升級、上線前的完整檢查清單,留到下一篇專門處理。自架的服務清單、映像版本、環境變數名稱都可能隨 Supabase 版本演進而調整,正式導入前一律以官方文件(supabase/supabaserepo 的docker/目錄)為準。
核心概念
在動手之前,先建立兩張心智圖:一是「這套堆疊由哪些服務組成、流量怎麼進來」,二是「哪些 .env 設定是絕對不能沿用預設值的地基」。
一、自架堆疊:一組協同運作的容器
Supabase 自架的官方推薦方式就是 Docker Compose。你平常在雲端點一下就有的「一個 Supabase 專案」,拆開來看其實是十幾個獨立服務組合而成,自架就是把這些服務全部以容器形式在同一台機器上跑起來。它們的協作關係大致如下:
[外部流量]
│ HTTPS (443)
▼
Caddy / Nginx ← 反向代理,處理 TLS 憑證
│ HTTP (8000)
▼
Kong (API Gateway) ← 唯一對外入口,負責路由與速率限制
│
├──► Auth (GoTrue) JWT 認證、OAuth、使用者管理
├──► PostgREST 從 schema 自動生成 REST API
├──► Realtime WebSocket 廣播資料庫變更
├──► Storage S3 相容的物件儲存 API
└──► Edge Runtime Deno-based Edge Functions 執行環境
│
postgres-meta ─────┐ 資料庫管理 API(供 Studio 使用)
Studio (管理介面) ──┤
▼
PostgreSQL (5432) ← 所有服務最終都連到這裡
把每個關鍵服務的職責列成一張表,會更清楚:
| 服務(compose 名稱) | 職責 |
|---|---|
| db(PostgreSQL) | 核心資料庫,所有資料與擴充功能都在這裡 |
| kong | API 閘道,唯一對外入口,負責路由、速率限制 |
| auth(GoTrue) | JWT 身份驗證、OAuth 登入、使用者管理 |
| rest(PostgREST) | 從資料庫 schema 自動生成 REST API |
| realtime | 透過 WebSocket 廣播資料庫變更 |
| storage | S3 相容的物件儲存 API |
| studio | 你熟悉的管理介面(Dashboard) |
| meta(postgres-meta) | 資料庫管理 API,Studio 靠它讀寫 schema |
除了上表這些核心服務,依版本不同,堆疊裡通常還會有 imgproxy(圖片轉換)、supavisor(連線池)、以及選用的 analytics(Logflare)、vector(日誌收集)等容器。確切的服務清單與映像版本請以你 clone 到的 docker-compose.yml 為準,不同 self-hosted 版本會有差異。
這張圖有兩個重點必須先記住:
- Kong 是唯一對外入口。 前端與後端呼叫的所有 API(auth、rest、realtime、storage、functions),全部先打到 Kong 的 8000 port,再由 Kong 路由到對應的內部服務。你不會、也不該直接連到 PostgREST 或 GoTrue 的內部 port。
- 反向代理在 Kong 之前。 生產環境要在 Kong 前面再擺一層 Caddy 或 Nginx 處理 HTTPS——Kong 本身只講 HTTP,TLS 憑證由反向代理負責。
二、.env:那些絕對不能沿用預設的地基
官方的 docker 目錄附了一份 .env.example,裡面每個變數都有預設值。這份預設值是公開在 GitHub 上、全世界都看得到的,所以自架的第一條鐵律就是:上線前把所有密碼與金鑰全部換成隨機值。 以下是幾個最關鍵、務必自己生成的變數:
| 變數 | 作用 | 為何關鍵 |
|---|---|---|
POSTGRES_PASSWORD | PostgreSQL superuser 密碼 | 用預設值等於資料庫門戶大開 |
JWT_SECRET | 簽署/驗證所有 JWT 的密鑰 | 至少 64 字元;所有 key 都由它簽出 |
ANON_KEY | 前端用的匿名金鑰(可公開) | 必須用你的 JWT_SECRET 重新簽 |
SERVICE_ROLE_KEY | 後端用的萬能金鑰(繞過 RLS) | 絕不可外洩,必須用你的 secret 重簽 |
DASHBOARD_USERNAME / DASHBOARD_PASSWORD | Studio 登入帳密 | Studio 沒它就人人可進 |
SITE_URL | 應用程式的對外網址 | 認證的信件連結、OAuth 導回都靠它 |
SUPABASE_PUBLIC_URL / API_EXTERNAL_URL | Supabase 對外可見的 URL | Studio 與 API 產生連結時需要 |
這裡有兩個很多人踩坑的關鍵細節:
ANON_KEY與SERVICE_ROLE_KEY不是獨立亂數,而是「用JWT_SECRET簽出來的 JWT」。 換句話說,你不能只改JWT_SECRET卻沿用.env.example裡的舊 key——那兩把 key 是用官方的示範 secret 簽的,和你的新 secret 對不上,認證會全數失效。正確流程是:先定好你的JWT_SECRET,再用它重新簽出這兩把 key(下面實作會示範官方腳本怎麼做)。SITE_URL決定認證流程導回哪裡。 使用者收到的驗證信、重設密碼信裡的連結,以及 OAuth 登入後導回的位置,都以SITE_URL為基準。沒設對,使用者點信裡的連結會被導到localhost或錯誤網址,認證流程直接卡死。
三、關鍵術語速查
| 名稱 | 一句話理解 |
|---|---|
| Kong | API 閘道,所有對外 API 的唯一入口 |
| 反向代理(Caddy/Nginx) | 在 Kong 前處理 HTTPS 的那一層 |
JWT_SECRET | 簽所有 JWT 的密鑰,anon/service_role 都由它簽出 |
anon key | 可公開、給前端用、遵守 RLS |
service_role key | 不可公開、給後端用、繞過 RLS |
SITE_URL | 你的 App 對外網址,認證信與 OAuth 導回都靠它 |
一句話記法:Kong 是唯一入口、反向代理管 HTTPS、JWT_SECRET 是簽章地基、service_role 是萬能鑰匙絕不外洩、SITE_URL 決定認證導回哪裡。
實作範例
下面把「從零到登入 Studio」拆成六步:① 取得官方 docker 設定 → ② 生成金鑰 → ③ 設定 .env → ④ docker compose up → ⑤ 登入 Studio → ⑥ 加上反向代理與 HTTPS。
一、取得官方 Docker 設定
官方把自架所需的所有檔案放在 supabase/supabase repo 的 docker/ 目錄。最直接的做法是 clone 下來,把 docker 目錄複製成你的專案:
# 只拉取需要的 docker 目錄(節省頻寬)
git clone --depth 1 https://github.com/supabase/supabase
# 建立你的專案目錄,把官方 docker 目錄複製進去
mkdir supabase-project
cp -rf supabase/docker/. supabase-project
cd supabase-project
# 以範例檔為基礎建立自己的 .env
cp .env.example .env
# 先把映像檔拉下來(可選,提早發現網路問題)
docker compose pull
此時目錄裡最重要的是三樣東西:docker-compose.yml(所有服務的定義)、.env(所有設定值)、以及 volumes/(資料庫初始化 SQL 與各服務設定)。先別急著 up——直接用 .env.example 的預設值上線是最大的安全事故來源。
二、生成金鑰(這一步不能跳過)
官方在 docker/ 底下附了金鑰生成腳本。最省事的做法是直接執行:
# 生成 JWT_SECRET、並用它重新簽出 ANON_KEY 與 SERVICE_ROLE_KEY
sh utils/generate-keys.sh
如果你的版本沒有這支腳本,或想手動掌控每一把金鑰,可以用 openssl 生成隨機值:
# JWT Secret(至少 64 字元;anon/service_role 都用它簽)
openssl rand -base64 48
# PostgreSQL 密碼
openssl rand -base64 32
# Vault 加密金鑰(需精確 32 字元)
openssl rand -hex 16
# Realtime DB 加密金鑰(需精確 16 字元)
openssl rand -hex 8
關於 ANON_KEY 與 SERVICE_ROLE_KEY:這兩把 key 本質上是「用 JWT_SECRET 簽出來、payload 分別是 role: anon 與 role: service_role 的 JWT」。它們必須和你剛生成的 JWT_SECRET 配對——generate-keys.sh 會自動幫你用新的 secret 簽好;若你手動生成 secret,則需用官方文件提供的 JWT 產生工具、帶入你的 secret 重新簽出這兩把 key。千萬不要只換 JWT_SECRET 卻沿用 .env.example 裡的舊 key,否則簽章對不上,登入與 API 呼叫會全面失效。
三、設定 .env 關鍵欄位
把生成好的值填進 .env。以下是最關鍵的幾組(其餘加密金鑰如 VAULT_ENC_KEY、SECRET_KEY_BASE 等,同樣用上面 openssl 的方式填入):
# === 資料庫 ===
POSTGRES_PASSWORD=用-openssl-生成的強密碼
POSTGRES_DB=postgres
POSTGRES_PORT=5432
# === JWT(認證地基)===
JWT_SECRET=你的-至少64字元-隨機密鑰
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # 用上面 JWT_SECRET 簽出
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # 用上面 JWT_SECRET 簽出
# === Studio 登入 ===
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=一組夠強的-Studio-登入密碼
# === 對外 URL(決定認證導回與連結)===
SITE_URL=https://app.yourdomain.com # 你的前端 App 網址
SUPABASE_PUBLIC_URL=https://supabase.yourdomain.com # Supabase 對外網址
API_EXTERNAL_URL=https://supabase.yourdomain.com # API 對外網址
# === Email(自訂 SMTP,強烈建議設定)===
SMTP_ADMIN_EMAIL=admin@yourdomain.com
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=465
SMTP_USER=你的-smtp-帳號
SMTP_PASS=你的-smtp-密碼
SMTP_SENDER_NAME=Supabase
.env 存的全是機密,務必確認它已被 .gitignore 排除、永遠不進版本控制。生產環境更建議改用外部 secret 管理器(如 Doppler、HashiCorp Vault、AWS Secrets Manager)注入,而不是把明文 .env 放在伺服器上。
四、啟動整套堆疊
金鑰與 .env 都就緒後,就能把整套服務跑起來:
# 在背景啟動所有服務(-d = detached)
docker compose up -d
第一次啟動會拉取十幾個映像、初始化資料庫,需要幾分鐘。啟動後用 ps 確認每個容器都健康:
# 查看所有服務狀態,每個應為 running / healthy
docker compose ps
# 若有服務起不來,看它的日誌找原因(例如 db、kong)
docker compose logs db
docker compose logs kong
常見的第一次啟動問題,幾乎都指向同一個根因:金鑰沒對齊。例如 auth 或 rest 一直重啟、日誌報 JWT 相關錯誤,通常就是 ANON_KEY/SERVICE_ROLE_KEY 不是用當前 JWT_SECRET 簽的。改對之後,用 docker compose down && docker compose up -d 重新來過(或依你版本提供的 recreate 指令強制重建)。
五、登入 Studio
所有容器 healthy 之後,Studio 就可以存取了。在還沒設反向代理的階段,先透過 SSH tunnel 從本機安全地連到 Studio,而不是把它的 port 對外開放:
# 從你的本機,把伺服器上的 Studio(經 Kong)映射到本機 8000
ssh -L 8000:localhost:8000 user@your-server
然後在瀏覽器打開 http://localhost:8000,會跳出 HTTP Basic Auth——輸入你在 .env 設的 DASHBOARD_USERNAME 與 DASHBOARD_PASSWORD 即可進入。進去後你會看到熟悉的 Table Editor、SQL Editor、Auth、Storage 等介面,和雲端版幾乎一致,差別只在這一整套現在跑在你自己的機器上。
小提醒:Studio 的存取權只靠這組 Dashboard 帳密保護。生產環境務必搭配反向代理的 HTTPS、IP 限制或放到內網,別讓 Studio 直接裸奔在公網上。
六、加上反向代理與 HTTPS
要真正上線,最後一步是在 Kong 前面架一層反向代理處理 HTTPS。Caddy 最省事,因為它會自動申請並續期 Let’s Encrypt 憑證:
# Caddyfile —— 自動處理 HTTPS,把流量轉發到 Kong 的 8000
supabase.yourdomain.com {
reverse_proxy localhost:8000
# Realtime 需要 WebSocket 升級
@websocket {
header Connection *Upgrade*
header Upgrade websocket
}
handle @websocket {
reverse_proxy localhost:8000
}
}
若你習慣用 Nginx,對應的設定重點是:監聽 443、掛上憑證、把 / 反向代理到 localhost:8000,並記得帶上 WebSocket 需要的 Upgrade 標頭:
server {
listen 443 ssl;
server_name supabase.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # 停用老舊的 TLS 1.0/1.1
location / {
proxy_pass http://localhost:8000; # 轉發到 Kong
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP 一律導向 HTTPS
server {
listen 80;
server_name supabase.yourdomain.com;
return 301 https://$host$request_uri;
}
搭配防火牆,只對外開放 80 與 443,把 Kong 的 8000、PostgreSQL 的 5432、Studio 統統留在內部:
# UFW 防火牆:只開放必要 port
ufw default deny incoming
ufw allow ssh # SSH(建議改非標準 port)
ufw allow 80/tcp # HTTP(自動導向 HTTPS)
ufw allow 443/tcp # HTTPS
# 刻意不開放 5432(DB)、8000(Kong)、3000(Studio)
走到這裡,你就有了一套「HTTPS 對外、Kong 唯一入口、資料庫與 Studio 藏在內網」的自架 Supabase,前端只要把連線網址指向 https://supabase.yourdomain.com、帶上你的 anon key 就能開始用了。
常見錯誤與最佳實踐
坑一:直接用 .env.example 的預設密碼與金鑰上線。
這是自架最致命、也最常見的事故。預設值全部公開在 GitHub,等於把資料庫密碼、JWT_SECRET、service_role 萬能金鑰、Studio 帳密全設成人盡皆知。任何人知道你網址就能用預設 service_role key 繞過所有 RLS、讀寫你全部資料。正確做法:取得 docker 設定後第一件事就是跑 generate-keys.sh(或 openssl rand)換掉每一把金鑰與密碼,且 ANON_KEY/SERVICE_ROLE_KEY 必須用你的新 JWT_SECRET 重新簽出。
坑二:只換 JWT_SECRET,卻沿用舊的 ANON_KEY/SERVICE_ROLE_KEY。
很多人以為換掉 secret 就安全了,結果 auth、rest 容器不斷重啟、前端一登入就報 JWT 錯誤。原因是那兩把 key 是「用某把 secret 簽出的 JWT」,和你的新 secret 對不上、簽章驗不過。正確做法:secret 與兩把 key 是一組,必須用同一把 JWT_SECRET 一起重新簽;generate-keys.sh 會自動處理好這層對齊。
坑三:把 Kong(8000)或 PostgreSQL(5432)直接對外暴露。
在 docker-compose.yml 留著 ports: "5432:5432",或讓 8000 裸奔公網,等於把資料庫與 API 閘道開放給全世界掃描攻擊。正確做法:對外只走反向代理的 443;PostgreSQL 改用 expose(只在 Docker 內部可見)而非 ports;需要遠端連資料庫時走 SSH tunnel(ssh -L 5432:localhost:5432 user@server);防火牆只放行 80/443/SSH。
坑四:沒設 SITE_URL,或沿用預設的 localhost。
SITE_URL 沒設對,使用者收到的驗證信、重設密碼信連結會指向 localhost,OAuth 登入後也導不回你的 App,整個認證流程直接卡死。正確做法:把 SITE_URL 設成你前端 App 的真實對外網址,SUPABASE_PUBLIC_URL/API_EXTERNAL_URL 設成 Supabase 的對外網址,三者都用 https://。
坑五:沿用內建的預設寄信、沒設自訂 SMTP。
自架版內建的寄信能力有嚴格速率限制,一旦使用者稍多,驗證信、重設密碼信就會發不出去。正確做法:在 .env 設好自訂 SMTP(SMTP_HOST/SMTP_USER/SMTP_PASS 等),用你自己的寄信服務(如 Resend、SES、Postmark),認證信才送得穩。
最佳實踐總結:
- 第一件事就是換金鑰:
generate-keys.sh換掉所有預設密碼與 key,JWT_SECRET至少 64 字元 - key 與 secret 是一組:
ANON_KEY/SERVICE_ROLE_KEY必須用你的JWT_SECRET重簽,不可沿用舊值 - 只留一個對外入口:反向代理走 443、Kong 走內部 8000、DB 與 Studio 藏內網,防火牆只開 80/443/SSH
SITE_URL一定要對:認證信連結與 OAuth 導回全靠它- 自訂 SMTP 別省:內建寄信有速率限制,生產務必接自己的寄信服務
.env絕不進 git:確認在.gitignore,生產改用外部 secret 管理器- 細節以官方為準:服務清單、映像版本、環境變數名稱都可能隨版本調整
小結
這是 Supabase 系列教學的第 043 篇,也是自架段(SB-5)的第一篇。上一篇《型別生成與 CI/CD》收尾了雲端這一側——用 GitHub Actions 把型別守門、遷移部署、函式上線全部自動化;從這一篇開始,我們換一條路線,走進自架:用 Docker Compose 把整套 Supabase 架在自己的伺服器上。
核心觀念濃縮成一句話:自架就是用 docker compose up 把一組協同運作的容器(db、kong、auth、rest、realtime、storage、studio、meta)跑在自己機器上;上線前務必用 generate-keys.sh 換掉所有預設密碼與金鑰,ANON_KEY/SERVICE_ROLE_KEY 要用你的 JWT_SECRET 重新簽,設好 POSTGRES_PASSWORD 與 SITE_URL,再用 Caddy/Nginx 反向代理處理 HTTPS、只讓 Kong 當唯一對外入口,最後登入 Studio 開始用。
- 堆疊全貌:十幾個容器協作,
Kong是唯一對外入口,反向代理在它前面管 HTTPS - 金鑰地基:
JWT_SECRET簽出anon/service_role,預設值全部要換,兩把 key 要跟 secret 對齊 .env關鍵:POSTGRES_PASSWORD、SITE_URL、Studio 帳密、自訂 SMTP 一個都不能漏- 上線最後一哩:反向代理走 443、DB 與 Studio 藏內網、防火牆只開 80/443/SSH
自架跑起來只是開始——真正的挑戰在「跑得久、跑得穩」。下一篇《備份、升級與上線檢查》,我們會補上自架最關鍵的維運面:自架沒有內建自動備份,你得自己用 pg_dump 或 WAL-G 規劃備份與 PITR;還有怎麼安全地升級版本、以及一份上線前該逐項打勾的生產環境檢查清單。把這篇的「架起來」和下一篇的「養得好」接起來,才是一套能真正扛住生產流量的自架 Supabase。