SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

VPSでZitadelをDockerセルフホストする方法

Zitadelの本番推奨は4コア・8GBです。DockerでPostgreSQL、masterkey、TLS、SMTP、バックアップを構成し、最初のアップグレードでデータベースに起きることも確認します。

VPS で Zitadel をセルフホストするために必要なもの

VPS で Zitadel をセルフホストするには、Docker ホスト、VPS を指すパブリック DNS 名、PostgreSQL、約 4 CPU コア、8 GB の RAM が必要です。Zitadel は identity provider です。OIDC (OpenID Connect) と SAML (security assertion markup language) でトークンを発行するため、他のサービスで個別にユーザー一覧を管理する必要がなくなります。インストールは curldocker compose up で構成します。運用を継続できるかどうかを左右するのは、masterkey、データベースユーザー、SMTP (simple mail transfer protocol)、バックアップ、そして最初のアップグレードです。

以下では、Ubuntu 24.04、Compose plugin 付きの Docker Engine 24 以降、そして auth.example.com のような名前がすでにサーバーを指して解決できることを前提とします。

Zitadel にはどの程度の VPS が必要か

Zitadel の Compose クイックスタートでは、2 GB の RAM を求めています。この数値はノートパソコンを前提にしています。Zitadel の本番環境向けガイドでは、異なる値が示されています。

ChartZitadel's own published sizing guidance, August 2026
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 プロセス自体は小さく、アイドル時の RAM 使用量は約 0.5 GB です。CPU コアはパスワードハッシュ処理に必要です。この処理は意図的に低速なため、ログインが集中すると CPU 使用率が急増します。もう一方の要素は PostgreSQL です。同じガイドでは、100 requests per second あたり約 1 コア、1 コアあたり 4 GB の RAM を見積もっています。これらを合算すると、単一ノードについてガイドが示す 4 コアと 8 GB、またはログとメトリクスを有効にした場合はノードあたり 16 GB になります。

そのため、2 GB の VPS でもこのスタックは起動しますが、実用環境としてプロジェクトが推奨する容量を下回ります。ログインサービスは、他のすべてのサービスが依存する基盤です。停止すると、そのサービスを信頼するすべてのサービスでログインできなくなります。認証に 8 GB を割り当てるのは高すぎると判断するのも妥当です。その判断は、移行後よりも今の段階で行うほうがはるかに低コストです。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(transport layer security)を終端します。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 リポジトリにコミットしてください。そうしないと、翌月に新しいマシンで同じコマンドを実行したときに別のファイルが取得され、何が変わったのか分からなくなります。

Postgres 専用ユーザーと実際のパスワードを設定する

出荷時の .env は、パスワード postgres を使い、superuser として Zitadel を PostgreSQL に接続します。

POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=postgres
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:postgres@postgres:5432/zitadel?sslmode=disable

ここでの hardening 手順には注意点があります。Zitadel のドキュメントでは POSTGRES_ZITADEL_PASSWORD.env に追加するよう指示されています。しかし、元の docker-compose.yml はその変数を読み取らないため、設定しても何も変わりません。一方、POSTGRES_ADMIN_PASSWORD だけを変更すると接続が失敗します。パスワードは DSN (data source name) 文字列にもリテラルで記述されているためです。Zitadel の接続方法を決めるのは、この DSN の行です。

.env.example 内のコメントには、残りの点も明記されています。DSN を設定すると、Zitadel はそのユーザーを直接使用し、権限を制限したユーザーを自動作成しません。そのため、最初の起動前に role を作成する必要があります。パスワードを生成し、Postgres だけを起動して、role を作成します。

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 呼び出しは、コンテナ内のローカルソケット経由で実行されます。公式の Postgres イメージはこの接続を信頼するため、パスワードは要求されません。重要なのは所有権です。PostgreSQL 15 以降では、単純な GRANT ALL PRIVILEGES ON DATABASE では role が public schema にテーブルを作成できなくなりました。そのため、schema の作成中に Zitadel のセットアップフェーズが権限エラーで失敗します。role に database と schema の所有権を与えることで、この問題を避けられます。

次に DSN を新しい role に変更し、ファイル内で実際の admin パスワードも設定します。

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 ネットワークからのみ到達可能で、ポートも host に公開されていないためです。最初の完全な起動後に、role が実際にデータを所有していることを確認します。

docker compose exec -T postgres psql -U zitadel -d zitadel -c '\dn'

eventstore schema と projections schema が表示されるはずです。空の一覧が表示された場合、セットアップフェーズはそこまで進んでいません。API コンテナのログに理由が記録されています。

マスターキーと、失った場合の影響

Zitadel は、保存する前にシークレットを暗号化します。対象には、クライアントシークレット、identity provider の認証情報、SMTP パスワード、one-time-password の seed、machine key があります。マスターキーは、これらすべての暗号化を解除します。マスターキーは正確に 32 文字で、ドキュメントにはその影響が明記されています。暗号化されたデータへのアクセスを失わずに変更することはできません。

マスターキーを 1 つ生成し、.env のプレースホルダー行を置き換えます。

tr -dc A-Za-z0-9 </dev/urandom | head -c 32; echo

ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters の行を編集し、2 行目を追加しないでください。Compose は同じキーが複数ある場合、最後の定義を使用します。そのため追加しても動作しますが、マスターキーの行が 2 つあるファイルは、次に読む人が混乱する原因になります。

次に、そのキーの保存場所を確認します。compose ファイルは、次のように API コンテナを起動します。

command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"

したがって、マスターキーはコンテナのコマンドラインに表示されます。docker inspect にアクセスできる人なら、その値を確認できます。単一管理者の VPS では、これは許容できるトレードオフです。また、.env の mode により、ディスク上のファイルが保護されます。これが許容できない場合は、キーをファイルとして mount し、代わりに --masterkeyFile /run/secrets/zitadel-masterkey を使用してください。これにより、値がプロセス引数に表示されなくなります。

初回起動の前に、マスターキーを password manager にコピーしてください。マスターキーはデータベースダンプには含まれません。そのため、別のマスターキーで復元したダンプから作成した instance は、自身のシークレットを読み取れません。ダンプを保存するアーカイブとは別の場所にマスターキーを保管してください。1 つのバックアップを盗まれただけで、暗号化データとその鍵の両方が漏えいする事態を防げます。

初回起動前に外部ドメインを設定する

ZITADEL_DOMAIN.env はコンテナ内の ZITADEL_EXTERNALDOMAIN に渡されます。これはユーザーが入力する名前です。Zitadel はこの値から OIDC issuer、ログイン画面のベース URI、SAML エンドポイント、最初の admin のログイン名を決定するため、単なる表示用の設定ではありません。

ZITADEL_DOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=true

Zitadel は Host ヘッダーから、接続先の instance を判定します。このヘッダーが認識済みのドメインと一致しない場合、すべてのリクエストに同じ応答を返します。

ID=QUERY-1kIjX Message=Instance not found

これは self-hosted Zitadel で最もよく発生するエラーです。ほぼ必ず、原因は次のどちらかです。ZITADEL_DOMAIN が実際にアクセスしている名前と一致していないか、前段のプロキシが Host を upstream のアドレスに書き換えています。名前ではなくサーバーの IP アドレスにアクセスした場合にも発生します。

これらの値は後から変更できます。変更を反映するには、Zitadel の setup phase を再実行する必要があります。また、すでに登録済みの各アプリケーションには、以前の redirect URI がそのまま残ります。最終的な名前を今決めておく方が、後から移行するよりも手間がかかりません。

Let's Encrypt オーバーレイで TLS を終端する

公開ドメインでは、Zitadel の Let's Encrypt オーバーレイを追加します。これにより Traefik は ACME(自動証明書管理環境)の HTTP チャレンジに切り替わり、公開ポートが 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

このオーバーレイは API コンテナに ZITADEL_EXTERNALPORT: 443ZITADEL_EXTERNALSECURE: true も設定します。これにより、公開 URL と Zitadel が自身のために構築する URL が一致します。開始前に A レコードが名前解決できなければなりません。名前解決できないと HTTP チャレンジが失敗するためです。

すでに nginx またはロードバランサーで TLS を終端している場合は、代わりに docker-compose.mode-external-tls.yml を使用し、プロキシが送信する送信元アドレスの範囲を TRAEFIK_TRUSTED_IPS に設定します。Traefik は、その一覧に含まれるアドレスからの X-Forwarded-* ヘッダーだけを受け入れます。値が誤っていると転送されたプロトコルが破棄され、Zitadel は HTTPS サイトに対して http:// URL を構築するようになります。

上流プロキシには、Zitadel が厳密に求める役割が 2 つあります。API は gRPC を使用するため、バックエンドとは HTTP/2 で通信する必要があります。また、HostX-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 のテスト環境にあるコンテナ名です。使用環境の名前に置き換えてください。Zitadel を 443 以外のポートで提供する場合は、ポートをヘッダーに含めるために grpc_set_header Host $host:$server_port; を使用します。それ以外は通常の virtual host です。Zitadel 固有ではない部分については、nginx reverse proxy の設定を行ごとに読む解説で確認できます。

最初の管理者とパスワード変更の強制

最初の起動で、1 つのインスタンス、1 つの組織、1 人の人間管理者が作成されます。ログイン名は zitadel-admin@zitadel. と外部ドメインを連結したものです。ZITADEL_DOMAIN=auth.example.com の場合は次のようになります。

zitadel-admin@zitadel.auth.example.com

Password1! がパスワードです。独自のパスワードを設定した場合を除きます。Zitadel の上流側のデフォルトでは、初回ログイン時にパスワード変更を強制します。配布される compose ファイルでは、このデフォルトが上書きされています。

ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false

この行は .env から読み込まれるのではなく、docker-compose.yml にハードコードされています。そのため、独自の値は小さなオーバーレイファイルに記述します。ファイル名を 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"

Compose は -f フラグなしで実行した場合に限り、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 にログインし、パスワードを変更します。その後、ほかの設定を作成する前に、そのアカウントで 2 要素目を有効にします。ZITADEL_FIRSTINSTANCE_* の各値は、最初のインスタンス作成中にのみ適用されます。インスタンスの作成後に編集しても、まったく効果はありません。

パスワードリセットが SMTP の動作開始まで何も実行しない理由

メールを送信できない ID プロバイダーは、数週間にわたって問題が表面化しない形で壊れます。Zitadel は、ユーザー招待、アドレス確認、パスワードリセットリンク、ワンタイムコード、ドメイン申請通知の送信にメールを使用します。SMTP プロバイダーを設定していなくても、Console は処理を完了したと表示し、メッセージは送信先のない通知ワーカーに渡されます。デフォルトでは、そのワーカーに MaxAttempts: 3MaxTtl: 5m が設定されているため、数分間に数回再試行した後で停止します。リンクを待っているユーザーには何も通知されません。

Console のインスタンス設定で https://auth.example.com/ui/console/settings を開き、設定します。SMTP プロバイダーのフォームでは、送信元メールアドレス、送信元名、ホストとポート、ユーザー、SMTP パスワード、TLS の切り替えを指定します。保存する前に、フォームのテストボタンを使用してください。実際のメッセージが送信されるため、届くか届かないかで結果を確認できます。

対応する環境変数として ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST とその関連変数もあります。これらはインスタンスの作成時に適用されます。すでに実行中のスタックには影響しないため、既存のインスタンスでは Console を使用してください。

VPS からのメール配信について、通常問題になる点が 2 つあります。多くのプロバイダーは、新規アカウントの送信先ポート 25 をブロックします。そのため、受信者のメールサーバーへ直接送信すると、役に立つエラーもなくタイムアウトします。代わりに、ポート 587 の認証済みリレーを使用してください。また、送信ドメインに SPF(sender policy framework)と DKIM(domainkeys identified mail)のレコードを公開してください。設定しないと、リセットリンクがスパムフォルダーに入り、ユーザーにはメールが送信されなかった場合と同じように見えます。

ユーザーを招待する前に、動作を確認してください。使い捨てのユーザーを作成し、パスワードリセットを要求して、メッセージが届くことを確認します。届かない場合は、docker compose logs -f zitadel-api に SMTP の失敗原因が示されます。SMTP パスワードはデータベースに暗号化して保存されるため、masterkey が管理する対象が 1 つ増えます。

Postgres と masterkey は分けてバックアップする

Zitadel が認識するすべての情報は PostgreSQL に保存されています。その情報を復号するのが masterkey です。これらは別々の場所にバックアップします。

まずダンプを作成します。

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 で、そのディレクトリをオフサイトへ転送します。

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 --prune

restic init は初回の1回だけ実行します。ダンプと最後の2つのコマンドを /usr/local/bin/zitadel-backup.sh に記述し、毎晩実行します。

0 3 * * * /usr/local/bin/zitadel-backup.sh

.env と使用するすべての compose ファイルは、git でバックアップします。masterkey はこの原則の例外です。masterkey はパスワードマネージャーと、現在の restic リポジトリとは別の2番目の場所に保存します。データベースとその復号鍵を同じアーカイブに保存すると、暗号化されたシステムのバックアップとして機能しなくなるためです。

復元していないバックアップは、推測にすぎません。同じサーバー上の一時データベースに復元し、内容を確認します。

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_test

eventstore スキーマ内のテーブル一覧が表示されれば、ダンプは正常です。スキーマが存在しないというエラーが表示された場合、ダンプは正常ではありません。損失が発生しない日に問題を発見できたことになります。Compose スタックをバックアップしてアップグレードする一般的な手順は、ほぼそのままここにも適用できます。同じアーカイブに masterkey を保存しないことだけが、Zitadel 固有の対応です。

Zitadel をインスタンスを失わずにアップグレードする

アップグレードとは、.env のバージョンを更新してから、次の 2 つのコマンドを実行することです。

docker compose pull
docker compose up -d --wait

ログインするユーザーがいる環境で実行する前に、2 つ目のコマンドの動作を理解してください。コンテナのコマンドは start-from-init です。これはサービスの提供を開始する前に init フェーズと setup フェーズを実行します。setup フェーズではデータベースのマイグレーションが実行されます。つまり、バージョンを更新すると、コンテナの起動時に稼働中のデータベースへマイグレーションが自動で適用され、その間 --wait はヘルスチェックを待機します。これが、前述のリストアテストを省略できない理由です。

アップグレードの直前に、最新のダンプを取得してください。昨夜のダンプは別のものです。

メジャーバージョンを一気に上げないでください。v3 から v4 へ移行するには、まず v3.4.1 以降を使用している必要があります。v4 では従来の OIDC 署名鍵が削除されるため、移行した時点で古い鍵で署名されたトークンの検証に失敗するからです。Zitadel の技術アドバイザリ A-10017 に詳細があります。新しい v3 を、古いトークンの有効期限が切れるまで十分な期間実行してからアップグレードしてください。

docker compose logs -f zitadel-api で setup フェーズを監視してください。大規模な eventstore ではマイグレーションに数分かかります。Traefik はヘルスチェックに合格するまで API へルーティングしないため、その間サイトは停止します。実際に発生してから把握するのではなく、あらかじめ計画してください。

ロールバックは、古いタグに戻せば済むものではありません。マイグレーションの実行後は、古いバイナリが見つかったスキーマを理解できないため、ロールバックにはダンプのリストアが必要です。インスタンスで実際のユーザーを運用するようになったら、docker-compose.prodlike.yml へ移行してください。これは 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 を推奨しています。ログとメトリクスを有効にする場合は、ノードあたり 16 GB が必要です。PostgreSQL のリソースは別に見積もり、100 requests per second あたり約 1 コア、1 コアあたり 4 GB の RAM とします。Compose の quickstart は 2 GB 未満で起動できます。これは試用には十分ですが、他のサービスが依存するシステム向けにプロジェクトが推奨する量を下回ります。

Zitadel の masterkey を失うとどうなりますか?

masterkey で暗号化したデータは、暗号化されたままになります。Client secret、identity provider の認証情報、SMTP パスワード、one-time password の seed は復号できません。後から masterkey を変更することもできません。データベースの dump だけでは、動作するインスタンスを復元できません。dump には暗号文しか含まれず、鍵がないためです。masterkey は password manager に保存し、dump を保持するバックアップとは別の場所にも保管してください。両方を失った場合は、インスタンスを最初から再構築するしかありません。

Zitadel のパスワードリセットメールが届かないのはなぜですか?

SMTP provider が設定されていないか、設定した provider が配信できないためです。Zitadel は各通知を worker にキューイングし、デフォルトでは 3 回試行します。しかし、どの場合も Console では成功として報告されるため、失敗が表面化しません。instance settings で SMTP provider を設定し、そのフォームの test ボタンを使用してください。実際のメッセージが送信されます。VPS からは port 587 の認証付き relay を使用してください。多くの provider は送信 port 25 をブロックするためです。送信ドメインには SPF と DKIM のレコードも公開し、メールが spam としてフィルタリングされないようにします。

インストール後に Zitadel の外部ドメインを変更できますか?

はい。ただし、.env だけを編集しても変更できません。ZITADEL_EXTERNALDOMAINZITADEL_EXTERNALPORTZITADEL_EXTERNALSECURE を変更し、Zitadel に setup phase を再実行させて変更を反映します。登録済みのアプリケーションには以前の redirect URI が残るため、手動で更新する必要があります。また、Host header が Zitadel の認識するドメインと一致しないリクエストには、Instance not found が返されます。最初の起動前に最終的な名前を決めておけば、これらの問題を避けられます。