自架部署(Docker Compose):把整套 Supabase 架在自己的伺服器上 | Supabase 完整教學

2026/11/01
自架部署(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/supabase repo 的 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)核心資料庫,所有資料與擴充功能都在這裡
kongAPI 閘道,唯一對外入口,負責路由、速率限制
auth(GoTrue)JWT 身份驗證、OAuth 登入、使用者管理
rest(PostgREST)從資料庫 schema 自動生成 REST API
realtime透過 WebSocket 廣播資料庫變更
storageS3 相容的物件儲存 API
studio你熟悉的管理介面(Dashboard)
meta(postgres-meta)資料庫管理 API,Studio 靠它讀寫 schema

除了上表這些核心服務,依版本不同,堆疊裡通常還會有 imgproxy(圖片轉換)、supavisor(連線池)、以及選用的 analytics(Logflare)、vector(日誌收集)等容器。確切的服務清單與映像版本請以你 clone 到的 docker-compose.yml 為準,不同 self-hosted 版本會有差異。

這張圖有兩個重點必須先記住:

  1. Kong 是唯一對外入口。 前端與後端呼叫的所有 API(auth、rest、realtime、storage、functions),全部先打到 Kong 的 8000 port,再由 Kong 路由到對應的內部服務。你不會、也不該直接連到 PostgREST 或 GoTrue 的內部 port。
  2. 反向代理在 Kong 之前。 生產環境要在 Kong 前面再擺一層 Caddy 或 Nginx 處理 HTTPS——Kong 本身只講 HTTP,TLS 憑證由反向代理負責。

二、.env:那些絕對不能沿用預設的地基

官方的 docker 目錄附了一份 .env.example,裡面每個變數都有預設值。這份預設值是公開在 GitHub 上、全世界都看得到的,所以自架的第一條鐵律就是:上線前把所有密碼與金鑰全部換成隨機值。 以下是幾個最關鍵、務必自己生成的變數:

變數作用為何關鍵
POSTGRES_PASSWORDPostgreSQL superuser 密碼用預設值等於資料庫門戶大開
JWT_SECRET簽署/驗證所有 JWT 的密鑰至少 64 字元;所有 key 都由它簽出
ANON_KEY前端用的匿名金鑰(可公開)必須用你的 JWT_SECRET 重新簽
SERVICE_ROLE_KEY後端用的萬能金鑰(繞過 RLS)絕不可外洩,必須用你的 secret 重簽
DASHBOARD_USERNAME / DASHBOARD_PASSWORDStudio 登入帳密Studio 沒它就人人可進
SITE_URL應用程式的對外網址認證的信件連結、OAuth 導回都靠它
SUPABASE_PUBLIC_URL / API_EXTERNAL_URLSupabase 對外可見的 URLStudio 與 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 或錯誤網址,認證流程直接卡死。

三、關鍵術語速查

名稱一句話理解
KongAPI 閘道,所有對外 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。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →