Zitadel VPS Docker 自架:4 核心與 8 GB 設定
Zitadel 建議 4 個 CPU 核心與 8 GB RAM。本文示範在單一 VPS 設定 PostgreSQL、masterkey、TLS、SMTP 與備份,並說明升級如何影響資料庫。
在 VPS 上自行代管 Zitadel 的需求
若要在 VPS 上自行代管 Zitadel,您需要 Docker 主機、指向該主機的公開 DNS 名稱、PostgreSQL,以及約 4 個 CPU 核心和 8 GB RAM。Zitadel 是身分識別提供者。它透過 OIDC (OpenID Connect) 和 SAML (security assertion markup language) 發行 token,讓其他服務不必各自維護使用者清單。安裝內容包括一個 curl 和一個 docker compose up。決定服務能否持續運作的項目包括 masterkey、資料庫使用者、SMTP (simple mail transfer protocol)、備份,以及第一次升級。
以下內容均假設使用 Ubuntu 24.04、搭配 Compose plugin 的 Docker Engine 24 或更新版本,並且已有類似 auth.example.com 的名稱解析至該伺服器。
Zitadel 需要多少 VPS 資源?
Zitadel 文件中的 Compose quickstart 要求 2 GB RAM。這個數字是以筆記型電腦為基準。Zitadel 的 production guide 提供了不同的數據。
The data behind this chart
[
{
"config": "Process floor, no load",
"cpu_cores": 0.5,
"ram_gb": 0.5
},
{
"config": "Single node, reduced setup",
"cpu_cores": 4,
"ram_gb": 8
},
{
"config": "HA node, logs and metrics on",
"cpu_cores": 4,
"ram_gb": 16
}
]這些是公開的建議值,不是從實際執行中的伺服器測得的數據。請將它們視為問題的資源配置範圍。Zitadel 程序本身很小,閒置時約使用 0.5 GB RAM。核心數是供密碼雜湊使用的。密碼雜湊刻意設計得很慢,因此登入請求突然增加時,CPU 使用量也會隨之升高。PostgreSQL 是另一項主要資源需求:同一份指南估算每 100 requests per second 需要約 1 個核心,且每個核心需要 4 GB RAM。將兩者合併後,單一節點需要指南所列的 4 個核心與 8 GB RAM;啟用 logging 與 metrics 後,則需要每個節點 16 GB RAM。
因此,2 GB VPS 可以啟動這組 stack,但低於專案對實際用途的建議規格。登入服務是其他所有服務都依賴的元件。登入服務停止時,所有信任它的服務都不會允許使用者登入。若你認為將 8 GB RAM 用於 authentication 的成本過高,這是合理的決定;現在做出決定,也遠比遷移後才調整便宜。Keycloak、Authentik 與 Zitadel 的比較說明各方案的記憶體需求與維運工作量,而自架的 Authentik 伺服器通常是較小伺服器的選擇。
取得服務堆疊並固定版本
mkdir zitadel-compose && cd zitadel-compose
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.yml
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/.env.example
cp .env.example .env
chmod 600 .env該檔案定義了實際要執行的 4 個服務。Traefik 是反向代理:它會依路徑進行路由,並透過後文的 overlay 終止 TLS(傳輸層安全性)。zitadel-api 是監聽 8080 埠的 Go 二進位檔。zitadel-login 是在 /ui/v2/login 提供的登入介面。postgres 負責保存所有資料。Redis 快取與 OpenTelemetry collector 也位於同一檔案中,但由 Compose profiles 控制;除非明確啟用,否則不會啟動。
現在不要執行 docker compose up。第一次啟動會建立執行個體,而下方有幾項設定在之後無法變更,除非進行額外處理。
你複製的 .env 會固定自身的映像標籤:
ZITADEL_VERSION=v4.16.0
TRAEFIK_IMAGE=traefik:v3.7.7
POSTGRES_IMAGE=postgres:17.10-alpine目前的 v4 版本是 v4.17.1,於 2026 年 8 月 14 日發布。將 ZITADEL_VERSION 設為你要執行的版本,並維持在 v4 系列,不要追蹤最新版本。上方的 curl 會從 main 分支取得 docker-compose.yml;該分支沒有固定版本,因此請將這兩個檔案的副本都提交至 git repository。否則下個月在新主機上執行相同命令時,取得的檔案可能已經不同,而你也無法知道變更了哪些內容。
為 Postgres 建立專用使用者與實際密碼
隨附的 .env 會以超級使用者身分,使用密碼 postgres 連線至 PostgreSQL:
POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=postgres
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:postgres@postgres:5432/zitadel?sslmode=disable這裡的強化步驟有一個陷阱。Zitadel 文件要求將 POSTGRES_ZITADEL_PASSWORD 附加至 .env,但基礎 docker-compose.yml 從未讀取該變數,因此設定它不會產生任何作用。單獨變更 POSTGRES_ADMIN_PASSWORD 反而會中斷連線,因為 DSN(資料來源名稱)字串中也直接寫入了密碼。DSN 會決定 Zitadel 的連線方式。
.env.example 中的註解已清楚說明其餘事項:設定 DSN 後,Zitadel 會直接使用其中的使用者,不會替你建立權限受限的使用者。因此,該角色必須在首次啟動前存在。產生密碼,單獨啟動 Postgres,然後建立該角色。
tr -dc A-Za-z0-9 </dev/urandom | head -c 32; echo
docker compose --env-file .env -f docker-compose.yml up -d postgres
docker compose --env-file .env -f docker-compose.yml exec -T postgres \
psql -U postgres -d postgres <<'SQL'
CREATE ROLE zitadel LOGIN PASSWORD 'the-password-you-generated';
ALTER DATABASE zitadel OWNER TO zitadel;
SQL
docker compose --env-file .env -f docker-compose.yml exec -T postgres \
psql -U postgres -d zitadel -c 'ALTER SCHEMA public OWNER TO zitadel;'這些 psql 呼叫會透過容器的本機 socket 在容器內執行。官方 Postgres image 信任這類連線,因此不會要求輸入密碼。關鍵在於所有權。在 PostgreSQL 15 及更新版本中,單純的 GRANT ALL PRIVILEGES ON DATABASE 不再允許角色在 public schema 中建立資料表。因此,Zitadel 的設定階段會在建立 schema 時因權限錯誤而失敗。讓該角色擁有資料庫與 schema 即可避免此問題。
現在將 DSN 指向新的角色,並在同一個檔案中設定實際的管理員密碼:
POSTGRES_ADMIN_PASSWORD=a-32-character-random-string
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:the-password-you-generated@postgres:5432/zitadel?sslmode=disable這裡使用 sslmode=disable 沒有問題,因為 Postgres 只能透過私有的 Compose network 存取,且其埠未發布至主機。首次完整啟動後,確認該角色確實擁有其資料:
docker compose exec -T postgres psql -U zitadel -d zitadel -c '\dn'結果應列出一個 eventstore schema 與一個 projections schema。若清單為空,表示設定階段尚未執行到這一步;API container log 會說明原因。
主金鑰,以及遺失它的代價
Zitadel 會在儲存 secret 前先將其加密,包括 client secret、身分識別提供者憑證、SMTP 密碼、一次性密碼種子及 machine key。主金鑰可解鎖所有這些資料。主金鑰長度固定為 32 個字元,文件也明確說明後果:若不遺失對加密資料的存取權,就無法變更主金鑰。
產生主金鑰,並替換 .env 中的預留位置行:
tr -dc A-Za-z0-9 </dev/urandom | head -c 32; echo請編輯 ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters 那一行,不要再附加第二行。Compose 會採用重複 key 的最後一個定義,因此附加一行確實能運作;但檔案中若有兩行 masterkey,會讓下一位讀取檔案的人誤判。
現在請考慮主金鑰的存放位置。Compose file 會以如下方式啟動 API container:
command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"因此,主金鑰會出現在 container 的 command line 上,docker inspect 可讓任何能存取 Docker socket 的人看見它。在單一管理員使用的 VPS 上,這是可接受的取捨,而 .env 上的 mode 可保護磁碟中的主金鑰。若無法接受這種做法,請將主金鑰掛載為檔案,改用 --masterkeyFile /run/secrets/zitadel-masterkey,讓值不會出現在 process arguments 中。
第一次啟動前,請將主金鑰複製到 password manager。主金鑰不會出現在 database dump 中,因此在不同的主金鑰下還原 dump,會產生無法讀取自身 secret 的 instance。請將主金鑰存放在保存 dump 的 archive 以外的位置,避免單一遭竊的備份同時包含加密資料及其解密金鑰。
設定初次啟動前的外部網域
ZITADEL_DOMAIN 會在 .env 中提供給容器內的 ZITADEL_EXTERNALDOMAIN,這也是使用者輸入的名稱。Zitadel 會根據這個名稱產生 OIDC issuer、登入介面的基底 URI、SAML 端點,以及第一個管理員的登入名稱,因此這不是裝飾性設定。
ZITADEL_DOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=trueZitadel 會根據 Host 標頭判斷您要連線的 instance。若該標頭的值不符合它已知的網域,每個請求都會得到相同的回應:
ID=QUERY-1kIjX Message=Instance not found這是自架 Zitadel 最常見的錯誤,幾乎都代表兩種情況之一。第一,ZITADEL_DOMAIN 不是您瀏覽時使用的名稱。第二,前端的 proxy 將 Host 改寫成上游位址。使用伺服器的 IP 位址而非網域名稱瀏覽時,也會出現這個錯誤。
之後仍可變更這些值。Zitadel 必須重新執行設定階段,才能套用變更;而您已註冊的每個應用程式仍會保留舊的重新導向 URI。現在就選定最終名稱,比日後再搬遷便宜得多。
使用 Let's Encrypt overlay 終止 TLS
對於公開網域,加入 Zitadel 的 Let's Encrypt overlay。此 overlay 會將 Traefik 切換至 ACME(自動憑證管理環境)HTTP challenge,並將公開埠替換為 80 和 443,因此主機上不能有其他服務占用這兩個埠。
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.mode-letsencrypt.yml
echo 'LETSENCRYPT_EMAIL=ops@example.com' >> .env此 overlay 也會在 API 容器上設定 ZITADEL_EXTERNALPORT: 443 和 ZITADEL_EXTERNALSECURE: true,因此公開 URL 與 Zitadel 為自身建立的 URL 才能一致。啟動前必須先確認 A record 能解析,否則 HTTP challenge 會失敗。
如果您已在 nginx 或 load balancer 上終止 TLS,請改用 docker-compose.mode-external-tls.yml,並將 TRAEFIK_TRUSTED_IPS 設為 proxy 傳送請求時使用的來源位址範圍。Traefik 只會接受來自該清單中位址的 X-Forwarded-* 標頭,因此值設錯時,轉送的通訊協定會被捨棄,Zitadel 便會為 HTTPS 網站建立 http:// URL。
上游 proxy 有兩項 Zitadel 嚴格要求的工作。它必須使用 HTTP/2 與後端通訊,因為 API 使用 gRPC。它也必須原樣傳遞 Host 和 X-Forwarded-Proto: https。Zitadel 提供的 nginx 範例如下:
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/certs/selfsigned.crt;
ssl_certificate_key /etc/certs/selfsigned.key;
location /ui/v2/login {
proxy_pass http://login-external-tls:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
location / {
grpc_pass grpc://zitadel-external-tls:8080;
grpc_set_header Host $host;
grpc_set_header X-Forwarded-Proto https;
}
}其中的上游名稱是 Zitadel 測試環境中的容器名稱,請替換為您自己的名稱。如果您在 443 以外的埠上提供 Zitadel,請使用 grpc_set_header Host $host:$server_port;,讓埠號一併隨標頭傳遞。其餘設定就是一般的 virtual host;逐行閱讀 nginx 反向代理設定涵蓋非 Zitadel 專屬的部分。
初次管理員與強制變更密碼
第一次啟動會建立一個 instance、一個 organisation,以及一個人類管理員。登入名稱是 zitadel-admin@ 加上 zitadel.,再加上您的外部網域。因此使用 ZITADEL_DOMAIN=auth.example.com 時,登入名稱為:
zitadel-admin@zitadel.auth.example.com除非您自行設定,否則密碼是 Password1!。Zitadel 上游預設會在首次登入時強制變更密碼,而隨附的 compose 檔案會覆寫這項預設值:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false該行直接寫在 docker-compose.yml 中,而不是從 .env 讀取,因此請在自行建立的小型 overlay 中填入您自己的值。將檔案命名為 docker-compose.local.yml:
services:
zitadel-api:
environment:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS: you@example.com
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: "a-long-temporary-password"
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "true"只有在不使用 -f 旗標執行 Compose 時,Compose 才會自行載入 docker-compose.override.yml;而 Zitadel 指南中的每個指令都會傳入 -f,因此會停用這項行為。與其重複一長串不斷增加的旗標,不如在 .env 中固定檔案清單:
COMPOSE_FILE=docker-compose.yml:docker-compose.mode-letsencrypt.yml:docker-compose.local.yml現在啟動:
docker compose pull
docker compose up -d --wait--wait 會持續執行該指令,直到 healthcheck 通過為止。如果 API 容器始終無法通過檢查,Compose 會以 dependency failed to start: container zitadel-compose-zitadel-api-1 is unhealthy 停止,而 docker compose logs zitadel-api 會提供原因。首次啟動時,原因通常是 masterkey 長度或資料庫 DSN。
登入 https://auth.example.com/ui/console,變更密碼,然後在建立其他項目之前,為該帳戶啟用第二因素驗證。每個 ZITADEL_FIRSTINSTANCE_* 值只會在建立第一個 instance 時生效。instance 建立後,編輯這些值完全不會產生作用。
SMTP 正常前,密碼重設為何毫無反應
無法寄送電子郵件的身分提供者,問題可能會隱藏數週而不被發現。Zitadel 會針對使用者邀請、地址驗證、密碼重設連結、一次性代碼及網域宣告通知寄送電子郵件。未設定 SMTP 提供者時,Console 仍會回報操作已完成,但訊息會交給無處可寄的通知 worker。預設值會讓該 worker 使用 MaxAttempts: 3 和 MaxTtl: 5m,在幾分鐘內重試數次後停止。等待連結的人不會收到任何通知。
請在 Console 的執行個體設定 https://auth.example.com/ui/console/settings 中進行設定。SMTP 提供者表單會要求填寫寄件者電子郵件地址、寄件者名稱、主機與連接埠、使用者、SMTP 密碼,以及 TLS 切換開關。儲存前請使用表單中的測試按鈕,因為它會寄送實際訊息:訊息不是成功送達,就是未送達。
環境變數也有對應設定,包括 ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST 及其同類變數。它們只會在建立執行個體時套用。對於已在執行中的 stack,這些變數不會生效,因此現有執行個體應使用 Console 設定。
從 VPS 寄送郵件時,有兩點特別容易造成問題。多數提供者會封鎖新帳戶的對外 25 埠,因此直接寄送到收件者的郵件伺服器時,連線會逾時,且不會提供有用的錯誤訊息。請改用 587 埠上的已驗證 relay。此外,請為寄件網域發布 SPF(sender policy framework)和 DKIM(domainkeys identified mail)記錄,否則密碼重設連結會進入垃圾郵件匣;對使用者而言,這看起來就像郵件從未寄出。
邀請任何人之前,先完成驗證。建立一個暫用使用者,要求進行密碼重設,並確認訊息是否送達。若未送達,docker compose logs -f zitadel-api 會指出 SMTP 失敗原因。SMTP 密碼會以加密形式儲存在資料庫中,這也是由 masterkey 保護的一項資料。
分開備份 Postgres 與 masterkey
Zitadel 所知的一切都儲存在 PostgreSQL 中。負責解密這些資料的是 masterkey。請將兩者備份到不同位置。
先建立 dump:
sudo install -d -m 700 /srv/zitadel-backups
docker compose exec -T postgres \
pg_dump -U postgres -Fc zitadel > "/srv/zitadel-backups/zitadel-$(date +%F).dump"-Fc 是 custom format。它會在輸出時壓縮,pg_restore 可選擇性地讀取這種格式。exec -T 會停用終端機輸出。這很重要,因為此命令會由 cron 執行,且不會連接終端機。
接著使用 restic 將該目錄推送到異地。restic 會加密資料並執行去重:
export RESTIC_REPOSITORY="sftp:backup@backup.example.com:/srv/restic/zitadel"
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /srv/zitadel-backups
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prunerestic init 只在第一天執行一次。請將 dump 與最後兩個命令放入 /usr/local/bin/zitadel-backup.sh,並讓它每晚執行:
0 3 * * * /usr/local/bin/zitadel-backup.sh請將 .env 與使用的所有 compose 檔案納入 git 備份。masterkey 不適用於上述做法。它應存放在密碼管理器中,並另外存放於不屬於此 restic 儲存庫的位置。若同一個封存同時保存資料庫與其解密金鑰,就不再是加密系統的備份。
尚未還原過的備份只是猜測。請在同一台伺服器上還原至暫存資料庫,並檢查內容:
docker compose exec -T postgres createdb -U postgres zitadel_restore_test
docker compose exec -T postgres pg_restore -U postgres -d zitadel_restore_test \
< /srv/zitadel-backups/zitadel-2026-08-21.dump
docker compose exec -T postgres psql -U postgres -d zitadel_restore_test -c '\dt eventstore.*'
docker compose exec -T postgres dropdb -U postgres zitadel_restore_testeventstore schema 中列出資料表,表示 dump 確實有效。若出現 schema 不存在的錯誤,表示 dump 無效。及早發現這項問題,可以避免在真正需要還原時才付出代價。Compose stack 備份與升級的一般做法 同樣適用於此,幾乎不需要修改;唯一 Zitadel 特有的部分,是不要將 masterkey 放在同一個封存中。
升級 Zitadel 且不遺失執行個體
升級就是在 .env 中提高版本,然後執行兩個命令:
docker compose pull
docker compose up -d --wait在對供人員登入的環境執行第二個命令前,先了解它的作用。容器的命令是 start-from-init。它會先執行 init 和 setup 階段,再開始提供服務;其中 setup 階段會執行資料庫遷移。因此,提高版本會在容器啟動時,無人值守地對正式資料庫執行結構描述遷移,而 --wait 會在此期間等待 healthcheck。這正是前述還原測試不可省略的原因。
在升級前立即建立最新的 dump。昨晚的 dump 不同於升級前剛建立的 dump。
不要跨越主要版本直接升級。從 v3 移至 v4 前,必須先使用 v3.4.1 或更新版本,因為 v4 移除了舊版 OIDC 簽署金鑰。因此,跨越版本後,以舊金鑰簽署的 token 會立即無法驗證。Zitadel 的技術公告 A-10017 說明了這項變更。修正方式是先使用較新的 v3,等待舊 token 過期,再進行升級。
使用 docker compose logs -f zitadel-api 監看 setup 階段。大型 eventstore 的遷移可能需要數分鐘。Traefik 必須等到 healthcheck 通過後,才會將請求轉送至 API,因此網站會在這段期間停止服務。請事先規劃停機時間,不要等到升級時才發現。
Rollback 不是重新指定舊 tag。遷移執行後,舊版 binary 無法理解目前的結構描述,因此 rollback 必須還原 dump。執行個體開始承載實際使用者後,請改用 docker-compose.prodlike.yml。這個 overlay 會將 init 和 setup 設為獨立於 start 的步驟,讓你能主動觸發並監看遷移,而不是讓遷移成為容器重新啟動的附帶結果。
新身分識別提供者應指向的位置
在 Console 中建立專案,再於專案內建立應用程式。對於現代應用程式,請選擇 OIDC;Zitadel 會提供 client ID、client secret,以及位於 https://auth.example.com/.well-known/openid-configuration 的 discovery document。大多數支援單一登入的自架軟體,正是需要這些資訊。
許多軟體不支援單一登入,或僅在付費方案中提供。對於第一種情況,可在應用程式前方部署 oauth2-proxy,保護應用程式,將任何 HTTP 服務交由 Zitadel 進行存取控管。對於第二種情況,在規劃以尚未購買的功能為基礎進行遷移前,建議先閱讀 自架應用程式的 SSO 費用。
FAQ
自架 Zitadel 需要多少 RAM 和 CPU?
Zitadel 的正式環境指南建議,單一節點採用精簡設定時,約需要 4 個 CPU 核心與 8 GB RAM;啟用日誌與 metrics 時,每個節點需要 16 GB RAM。PostgreSQL 的資源需求需另外估算,約為每 100 requests per second 配置 1 個核心,且每個核心配置 4 GB RAM。Compose quickstart 可在 2 GB RAM 內啟動,足以用於試用,但低於專案對其他服務所依賴之系統的建議規格。
如果遺失 Zitadel masterkey,會發生什麼事?
所有使用該金鑰加密的資料仍會保持加密狀態。Client secrets、identity provider 憑證、SMTP 密碼與 one-time-password seeds 都無法解密,而且事後無法更換該金鑰。單獨使用 database dump 無法還原可正常運作的 instance,因為 dump 只包含 ciphertext,不包含金鑰。請將 masterkey 存放在 password manager 中,並置於與保存 dump 的備份不同的位置。如果兩者都遺失,只能從頭重建 instance。
為什麼 Zitadel 的密碼重設電子郵件始終收不到?
因為尚未設定 SMTP provider,或已設定的 provider 無法投遞郵件。Zitadel 會將每則通知排入 worker 的佇列,預設嘗試 3 次,並在 Console 中回報成功,因此失敗不會明顯顯示。請在 instance settings 中設定 SMTP provider,並使用該表單中的測試按鈕;此按鈕會實際寄出郵件。若使用 VPS,請在 port 587 上使用需要驗證的 relay,因為多數 provider 會封鎖對外 port 25;另外請為寄件網域發布 SPF 與 DKIM records,避免郵件被判定為 spam。
安裝後可以變更 Zitadel 的 external domain 嗎?
可以,但不能只編輯 .env。請變更 ZITADEL_EXTERNALDOMAIN、ZITADEL_EXTERNALPORT 與 ZITADEL_EXTERNALSECURE,然後讓 Zitadel 重新執行 setup phase,使其套用變更。已註冊的應用程式會保留舊的 redirect URIs,必須手動更新;任何 Host header 不符合 Zitadel 已知網域的 request,都會收到 Instance not found。在第一次啟動前決定最終名稱,可以避免這些問題。