如何用 Docker 在 VPS 自行代管 Supabase
了解如何在自有伺服器執行官方 Supabase Docker 堆疊:替換示範密鑰、掌握 11 個服務、評估記憶體需求,並安全備份與更新資料庫。
建置目標
自行代管 Supabase,表示在自己的伺服器上執行官方 Docker Compose 堆疊:Postgres、前端的 REST API、驗證服務、檔案儲存、即時 WebSocket,以及 Studio 儀表板。你只需複製一個儲存庫、編輯一個 .env 檔案,再啟動 11 個容器;這些容器會共同運作,形成一個由你管理的 Supabase 專案。
安裝流程很短。真正容易出問題的是 .env 檔案。該檔案內含已發佈在儲存庫中的示範密鑰;使用這些預設值啟動的堆疊,任何找到它的人都能存取。本指南說明必須替換的密鑰、各項服務的用途、堆疊實際需要多少記憶體,以及如何在不刪除資料庫的情況下更新堆疊。
如果你不熟悉 Compose,請先閱讀 VPS 上的 Docker Compose 基礎。以下所有步驟都假設 docker compose version 已能輸出版本。
實際包含哪些元件
Supabase 不是單一程式。Compose 檔案會在同一個網路上啟動一組獨立服務。了解各服務的用途後,才能將一長串容器名稱轉化為可診斷的問題。
db是載入 Supabase 擴充功能的 PostgreSQL。其他所有服務都會與它通訊。如果此容器狀態不健康,其他服務也會全部失敗。api-gw是 API gateway。自 August 2026 release 起,它改用 Envoy;在此之前使用名為kong的服務,而 Kong 仍可透過 opt-in override 啟用。它監聽 port 8000,並將/rest/v1/、/auth/v1/與/storage/v1/路由至正確的後端。這是唯一應對外公開的容器。rest是 PostgREST。它會讀取 Postgres schema,並將其提供為 REST API,因此新增資料表後,無須撰寫程式碼就會產生新的 endpoint。auth是 GoTrue。它會發行用來識別使用者的 JSON web tokens (JWT)。storage與imgproxy負責檔案上傳與影像調整大小。realtime會透過 websockets 串流資料庫變更。studio與meta分別是 dashboard 及其背後的 admin API。functions是以 Deno 為基礎的 edge runtime,supavisor則是 Postgres connection pooler。
Logs and analytics(Logflare 加上 vector)已於 June 2026 移出預設的 Compose 檔案。現在必須透過 sh run.sh config add logs 啟用 opt-in override file;這與 使用多個 Compose 檔案執行單一堆疊中所述的 Compose 檔案分層方式相同。
這份清單說明了下方資源數量的由來。你執行的不只是資料庫,而是資料庫加上十個支援服務。
規劃 8 GB RAM
截至 2026 年 7 月,全新安裝且尚未加入自己的資料或流量時,這套服務閒置約使用 2.5 至 3 GB 的常駐記憶體。在該次測量中,analytics 服務與 Studio 的 Node.js 程序是兩個最大的單一使用者。analytics 已不再包含於預設堆疊中,因此只有在啟用 logs override 時才會占用記憶體。2 GB 的伺服器會先啟動容器,接著其中一個容器會被 kernel 的 out-of-memory killer 終止;常見症狀是容器持續重新啟動,並以 exit code 137 結束。Supabase 自行維護的 self-hosting 文件在 2026 年 10 月查閱時,列出的最低需求為 4 GB RAM、2 個 CPU 核心及 40 GB SSD;建議配置則為 8 GB、4 個核心及 80 GB。
對於任何重要服務,請配置 8 GB RAM 與 4 vCPU。如果能接受同時執行大型查詢與使用 Studio 時速度變慢,4 GB 也足以支援單人開發環境。磁碟容量同樣重要,因為 Postgres、storage volume,以及啟用 logs 後的 log data 都位於 project directory 下。先從 40 GB 開始並持續監控。選擇方案前先統計服務數量,是自架任何服務都值得維持的習慣,因為 PhotoPrism 與 Immich 也有明確的 RAM 最低需求:Immich 的 quick start page 在 2026 年 10 月查閱時,要求至少 6 GB RAM 與 2 個 CPU 核心。如果目前專案還不需要這麼大的伺服器,請比較 Supabase 免費 cloud plan 實際包含哪些內容;若小型應用程式維持在 500 MB 以下,且一週內從不閒置,可能可以免費在該方案上執行。
安裝:複製官方儲存庫
支援的方式是將主要儲存庫中的 docker 目錄複製到您自行建立的專案目錄。這種分離很重要,因為後續執行 git pull 時,不會覆寫您的 .env。
git clone --depth 1 https://github.com/supabase/supabase
mkdir supabase-project
cp -rf supabase/docker/* supabase-project
cp supabase/docker/.env.example supabase-project/.env
cd supabase-project
docker compose pulldocker compose pull 會下載數 GB 的映像檔。完成後,每個服務都應標示為 Pulled。如果這裡出現 manifest unknown 錯誤,表示上游已移除指定的映像檔標籤。此時應重新拉取較新的儲存庫版本,不要手動修改標籤。
首次啟動前必須變更的密密碼
請在啟動 stack 前完成這些操作,不要等到啟動後才處理。其中幾個值會在首次啟動時寫入資料,因此之後才變更就必須重設資料庫。
repository 隨附一個 generator,能正確產生所有值,包括必須使用新的 JWT secret 簽署的 2 個 API keys。
sh utils/generate-keys.sh --update-env該 script 會將 JWT_SECRET、ANON_KEY、SERVICE_ROLE_KEY、SECRET_KEY_BASE、REALTIME_DB_ENC_KEY、VAULT_ENC_KEY、PG_META_CRYPTO_KEY、Logflare tokens、S3 protocol keys 和 MINIO_ROOT_PASSWORD 的新值寫入 .env。截至 October 2026,它也會為 POSTGRES_PASSWORD 和 DASHBOARD_PASSWORD 寫入隨機值。該 script 需要 openssl;任何正常的 Ubuntu image 都具備此項目。由於它現在也會重新寫入 database password,只有在首次啟動前才能搭配 --update-env 執行,絕對不要對已含有資料的 stack 執行。
仍有 2 個值值得在 .env 中手動檢查:
POSTGRES_PASSWORD。script 會填入隨機 hex,這樣即可安全使用。如果要換成自訂值,請只使用英文字母和數字。此處的標點符號會破壞數個服務透過串接字串建立的 connection strings。此類失敗看起來像 authentication error,而不是 parsing error,容易讓人朝錯誤方向排查。DASHBOARD_USERNAME和DASHBOARD_PASSWORD。這些是 Studio 的 basic authentication credentials。script 會設定隨機 password,但會將 username 保留為預設值supabase。隨附的預設 password 就是this_password_is_insecure_and_should_be_updated。因此,如果略過 script,請立即變更。
請了解 ANON_KEY 和 SERVICE_ROLE_KEY 不能自行捏造。兩者都是使用 JWT_SECRET 簽署的 JWT。gateway 只接受 .env 中的確切 keys,而後方的 services 會根據 JWT_SECRET 驗證簽章,因此不相符的 key 會以 HTTP 401 拒絕。預設的 Envoy gateway 會回傳 401 Unauthorized;目前的 Kong override 也會回傳相同結果。該 override 執行 Kong 3.9,並回傳 {"message":"Unauthorized"}。2026 年 3 月以前的安裝會執行 Kong 2.8,並回傳 {"message":"Invalid authentication credentials"}。這是 self-hosting 最常見的失敗原因:operator 變更了 JWT_SECRET,卻保留 demo keys。請務必同時產生這 3 個值。
請將 SERVICE_ROLE_KEY 視同 root password 管理。它能完全繞過 row level security。只能將它放在 server side code 中,不得放在其他位置。
將 SITE_URL 設為 app 的位址,並將 API_EXTERNAL_URL 設為 API 的公開位址,最後加上 /auth/v1,例如 https://supabase.example.com/auth/v1。將 SUPABASE_PUBLIC_URL 設為相同的公開位址,但不含 path。Auth 會根據這些值建立 email confirmation 和 OAuth callback links。若保留其 localhost 預設值,所有使用者都會被導向各自的電腦。
接著檢查目前的設定:
sh run.sh secrets啟動並確認服務狀態正常
sh run.sh start
docker compose psrun.sh start 包裝 docker compose up -d --wait,因此會持續等待,直到健康檢查通過。每個服務都應顯示 running (healthy) 或 running。首次啟動需要 2 到 4 分鐘,因為 Postgres 會先執行初始化指令碼,其他服務必須等到完成後才能連線。
如果容器持續重新啟動,請依服務名稱查看其日誌:
docker compose logs db
docker compose logs authStudio 會使用 8000 埠,並要求輸入您設定的 dashboard 使用者名稱與密碼。
請勿將連接埠 8000 暴露在公用網際網路上
8000 上的 gateway 使用明文 HTTP。所有 API key 和使用者密碼都會以明文在網路上傳送,而 Studio 憑證使用的是基本驗證,也就是 Base64 編碼,而非加密。
請在 gateway 前方設定反向代理,並在該處終止 TLS(傳輸層安全性),再將 gateway 綁定至 loopback 位址,讓其他主機無法連線。在 docker-compose.yml 中,api-gw 服務下方的 port 設定會改為 127.0.0.1:${API_GW_HTTP_PORT:-${KONG_HTTP_PORT:-8000}}:8000/tcp,代理再將請求轉送至該位址。在 2026 年 8 月切換至 Envoy 之前安裝的版本中,相同的 port 設定則位於名為 kong 的服務下。在多個 Compose 應用程式前方設定 Traefik 說明憑證的處理方式。同一個代理最終也會放在這台主機上其他所有服務的前方,從這個堆疊到像 將 Jellyfin 媒體庫重建成 90 年代錄影帶出租店 這類不太嚴肅的服務都包括在內,而每個服務都應使用主機名稱,不要再開放另一個連接埠。如果某個儀表板只有你自己會使用,可以略過代理,改用 SSH tunnel 存取 loopback port;self-hosted open-kritt 也採用相同方式,讓掃描介面完全不暴露在公用網際網路上。
也請在 firewall 中封鎖其餘連接埠,因為 Docker 會自行寫入 iptables 規則來發布連接埠,天真的 ufw 設定通常無法看見這些規則。為什麼 Docker 容器會忽略你的 ufw 規則 說明了這個陷阱。
備份資料庫,而不是備份目錄
Postgres 資料位於 ./volumes/db/data 的 bind mount 中。容器執行期間直接複製該目錄,會得到不完整的複本,因為 Postgres 會緩衝寫入,而磁碟上的檔案只有在 checkpoint 時才會保持一致。還原這類複本通常可以成功,但有時會悄悄遺失最後幾筆交易。對備份而言,這是最糟糕的失敗模式。
請改用 dump。pg_dumpall 會在容器內執行,並產生一致的快照:
docker exec -t supabase-db pg_dumpall -U postgres > supabase-$(date +%F).sql在信任檔案之前,先確認檔案不是空的。接著依排程將這些 dump 傳送到伺服器外部,這正是 使用 restic 建立加密的異地備份 的用途。排程 dump 若無聲失敗,和完全沒有備份沒有差別。因此,請讓 cron 或 systemd 工作在以非零狀態結束時 將警示推送到手機。同時備份你的 .env。遺失 JWT_SECRET 會使所有已發出的 token 失效,也會使所有儲存的加密 secret 無法讀取。
上傳的檔案位於 ./volumes/storage。這些是一般檔案,因此直接複製即可。
不遺失資料地更新
Supabase 會在 docker-compose.yml 中固定映像版本,因此除非手動更新,否則不會變更。手動組建的任何堆疊都值得採用版本固定,這也是自行代管的 RustDesk relay會固定兩個伺服器映像,而不追蹤浮動標籤的原因:升級應該在你有時間處理的早晨,由你自行決定。每次都要先建立傾印。
docker compose pull
sh run.sh recreaterecreate 會停止堆疊,再使用新的映像重新啟動。資料仍然存在,因為資料位於主機上的 bind mount,而不是容器內。進行主要版本升級前,請先閱讀儲存庫中的 CHANGELOG.md,因為 Postgres 的主要版本升級是獨立步驟,recreate 不會執行這項操作。自 June 2026 起,Postgres 17 一直是預設版本;現有的 Postgres 15 安裝可透過 sudo bash utils/upgrade-pg17.sh 升級至該版本。此指令會使用 pg_upgrade 就地遷移資料,並在每個主要步驟要求確認。執行時需要所有容器都在執行中,且可用磁碟空間至少為資料庫大小的兩倍再加上 5 GB。Supabase 仍建議在開始前另外建立獨立備份。
若要套用 Compose 檔案本身的變更,請在專案目錄中執行 sh update.sh;也可以先執行 sh update.sh --dry-run 查看可能的變更。此操作會將上游檔案與你自己的修改合併,不會覆寫 .env 中現有的值(只會附加新鍵),並會在寫入前將設定封存至 backups/,但不會封存資料庫。沒有 .supabase-version 檔案的安裝需要 --from,以及安裝開始時所使用的版本。這對 August 2026 的版本尤其重要,因為該版本以 api-gw 取代 kong 服務,並要求更新 docker-compose.yml。手動方式仍然可用:重新複製上游儲存庫,將其中的 docker 目錄複製到專案中,但請注意不要覆寫 .env。
完整重設會刪除包括資料庫在內的所有內容,並將 .env 還原為預設值(舊設定會保留為 .env.old)。這是另一個獨立的指令碼,執行時會要求確認:
sh reset.shFAQ
為什麼我的 API 呼叫會回傳「Invalid authentication credentials」?
這段確切訊息來自 Kong 2.8,也就是 2026 年 3 月以前安裝的堆疊所使用的 gateway。預設的 Envoy gateway,以及目前使用 Kong 3.9 執行的 Kong 覆寫設定,會將相同問題回報為一般的 401 Unauthorized。無論是哪一種情況,您傳送的金鑰都與 .env 中的 ANON_KEY 或 SERVICE_ROLE_KEY 不符,或是這些金鑰並非使用目前 .env 中的 JWT_SECRET 簽署。在從未存放資料的堆疊上,使用 sh utils/generate-keys.sh --update-env 一起重新產生這 3 個值。在執行中的堆疊上,不要帶入該 flag 執行指令並回答 no,讓指令只顯示新值;再手動將 JWT_SECRET、ANON_KEY 與 SERVICE_ROLE_KEY 複製到 .env,並保留 POSTGRES_PASSWORD 不變。接著執行 sh run.sh recreate,讓服務讀取新值。
我可以在 2 GB VPS 上執行自架 Supabase 嗎?
無法可靠執行。截至 2026 年 10 月,Supabase 的自架文件將 4 GB RAM 列為最低需求;截至 2026 年 7 月,包含目前改為選擇性啟用的 logging services 在內,堆疊閒置時約使用 3 GB。2 GB 主機會因 out of memory killer 而終止容器,並在 docker compose ps 中看到 exit code 137。正式環境請使用 8 GB,個人開發則將 4 GB 視為最低配置。
自架 Supabase 是否包含 edge functions?
包含。Compose 檔案包含以 Deno 為基礎的 functions runtime,並會提供您放在 ./volumes/functions 下的所有內容。不過,它不包含代管平台的 global deployment network,因此 functions 只會在您自己的單一伺服器、單一位置上執行。
如何直接連線到 Postgres 資料庫?
在伺服器本機使用 docker exec -it supabase-db psql -U postgres 開啟互動式 shell。若使用外部用戶端,請透過 5432 埠連線至 Supavisor,使用者為 postgres.<POOLER_TENANT_ID>,密碼為您的 POSTGRES_PASSWORD。請勿將該埠開放至網際網路。請透過 VPN 或 SSH tunnel 連線。
為什麼我的 auth 確認電子郵件會連結到 localhost?
.env 中的 SITE_URL 與 API_EXTERNAL_URL 仍是預設值。auth service 會根據這 2 個值建立所有確認連結與密碼重設連結,因此會傳送設定值指定的位址。請將兩者設為實際的公開 URL,然後重新建立堆疊。