SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Как самостоятельно установить Zitadel на VPS через Docker

Разверните Zitadel на собственном сервере с требованиями 4 ядра и 8 ГБ RAM. Настройте Postgres, TLS, SMTP и мастер-ключ, а также узнайте, как правильно выполнять обновления БД.

Что необходимо для самостоятельного размещения Zitadel на VPS

Для самостоятельного размещения Zitadel на VPS вам потребуется хост с Docker, публичное DNS-имя, указывающее на него, PostgreSQL, а также около 4 ядер CPU и 8 ГБ оперативной памяти. Zitadel — это провайдер идентификации. Он выдает токены по протоколам OIDC (OpenID Connect) и SAML (Security Assertion Markup Language), чтобы другие ваши сервисы перестали хранить собственные списки пользователей. Установка состоит из curl и docker compose up. Компоненты, от которых зависит работоспособность системы — это мастер-ключ, пользователь базы данных, SMTP (Simple Mail Transfer Protocol), резервное копирование и первое обновление.

Все инструкции ниже предполагают использование Ubuntu 24.04, Docker Engine версии 24 или новее с плагином Compose, а также доменное имя вроде auth.example.com, которое уже указывает на ваш сервер.

Сколько ресурсов VPS требуется для Zitadel?

В руководстве по быстрому запуску Zitadel через Compose указано требование в 2 GB оперативной памяти. Это значение приведено для локальной разработки. В руководстве по эксплуатации Zitadel для production указаны другие цифры.

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 потребляет немного ресурсов, около 0.5 GB оперативной памяти в режиме ожидания. Ядра процессора необходимы для хеширования паролей — этот процесс намеренно замедлен, поэтому всплеск попыток входа в систему вызывает скачок нагрузки на CPU. Вторая часть нагрузки приходится на PostgreSQL: в том же руководстве заложено примерно одно ядро на 100 запросов в секунду и 4 GB оперативной памяти на ядро. Сложив эти показатели, вы получите 4 ядра и 8 GB, указанные в руководстве для одного узла, или 16 GB на узел при включенном логировании и сборе метрик.

Таким образом, VPS с 2 GB памяти позволит запустить этот стек, но это меньше, чем проект рекомендует для реальной эксплуатации. Сервис авторизации — это компонент, от которого зависят все остальные службы. Если он недоступен, ни одно приложение, доверяющее ему, не пропустит пользователей. Решение о том, что 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

Этот файл определяет четыре сервиса, которые вы будете запускать. Traefik выступает в роли reverse proxy: он выполняет маршрутизацию по пути и, с учетом настроек overlay ниже, завершает TLS (transport layer security). zitadel-api — это бинарный файл Go на порту 8080. zitadel-login — интерфейс входа в систему, доступный по адресу /ui/v2/login. postgres содержит все компоненты. Кэш Redis и коллектор OpenTelemetry находятся в том же файле за профилями Compose и остаются выключенными, пока вы их не активируете.

Не запускайте 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, опубликованный 14 августа 2026 года. Установите ZITADEL_VERSION в значение версии, которую планируете использовать, и оставайтесь на ветке v4, вместо того чтобы отслеживать самую свежую версию. Указанный выше curl загружает docker-compose.yml из ветки main, которая ни к чему не привязана, поэтому сохраните копию обоих файлов в git-репозиторий. В противном случае та же команда на новом сервере в следующем месяце загрузит другой файл, и вы не узнаете, что именно изменилось.

Создание отдельного пользователя и пароля для Postgres

Поставляемый .env подключает Zitadel к PostgreSQL с правами суперпользователя и паролем postgres:

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 (data source name). 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 выполняются внутри контейнера через локальный сокет, которому официальный образ Postgres доверяет, поэтому пароль не запрашивается. Важен вопрос владения. В PostgreSQL 15 и новее обычный GRANT ALL PRIVILEGES ON DATABASE больше не позволяет роли создавать таблицы в схеме public, поэтому этап настройки Zitadel завершается ошибкой прав доступа при попытке создания схем. Назначение роли владельцем базы данных и схемы решает эту проблему.

Теперь укажите в 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, а его порт не публикуется на хост. После первого полного запуска убедитесь, что роль действительно владеет своими данными:

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

В выводе должны отобразиться схемы eventstore и projections. Пустой список означает, что этап настройки не был завершен; причину можно найти в логах контейнера API.

Masterkey и последствия его потери

Zitadel шифрует секреты перед сохранением: клиентские секреты, учетные данные провайдеров идентификации, пароль SMTP, сиды для одноразовых паролей и ключи машин. Masterkey открывает доступ ко всем этим данным. Он состоит ровно из 32 символов, и документация прямо предупреждает о последствиях: его нельзя изменить без потери доступа к зашифрованным данным.

Сгенерируйте ключ и замените строку-заполнитель в .env:

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

Редактируйте строку ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters, а не добавляйте вторую. Compose использует последнее определение повторяющегося ключа, поэтому добавление сработает, но файл с двумя строками masterkey — это ловушка для того, кто будет читать его в будущем.

Теперь подумайте, где хранится этот ключ. Файл compose запускает контейнер API следующим образом:

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

Таким образом, masterkey находится в командной строке контейнера, где docker inspect показывает его любому, у кого есть доступ к Docker socket. На VPS с одним администратором это допустимый компромисс, а права доступа к .env защищают его на диске. Если это неприемлемо, смонтируйте ключ как файл и используйте --masterkeyFile /run/secrets/zitadel-masterkey — это позволит не передавать значение в аргументах процесса.

Скопируйте masterkey в свой менеджер паролей перед первым запуском. Он не отображается в дампе базы данных, поэтому дамп, восстановленный с другим masterkey, создаст экземпляр, который не сможет прочитать собственные секреты. Храните его отдельно от архива с дампом, чтобы в случае кражи резервной копии злоумышленник не получил одновременно зашифрованные данные и ключ к ним.

Установка внешнего домена перед первым запуском

ZITADEL_DOMAIN в .env передается в ZITADEL_EXTERNALDOMAIN внутри контейнера, и именно это имя вводят ваши пользователи. Zitadel использует его для формирования OIDC issuer, базового URI интерфейса входа, SAML-эндпоинтов и имени входа первого администратора, поэтому это не просто косметический параметр.

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

Zitadel определяет, к какому экземпляру вы обращаетесь, на основе заголовка Host. Если этот заголовок не совпадает с известным доменным именем, на любой запрос будет приходить один и тот же ответ:

ID=QUERY-1kIjX Message=Instance not found

Это самая распространенная ошибка при самостоятельном хостинге Zitadel, и она почти всегда означает одно из двух. Либо ZITADEL_DOMAIN не совпадает с именем, которое вы вводите в браузере, либо прокси-сервер перед Zitadel перезаписывает Host на адрес upstream. Обращение к IP-адресу сервера вместо доменного имени также приводит к этой ошибке.

Вы можете изменить эти значения позже. Zitadel потребуется повторно запустить фазу настройки, чтобы применить изменения, при этом все уже зарегистрированные приложения сохранят свои старые redirect URI. Выбор окончательного имени сейчас потребует гораздо меньше усилий, чем его последующая смена.

Завершение TLS с помощью оверлея Let's Encrypt

Для публичного домена добавьте оверлей Let's Encrypt для Zitadel. Он переключает Traefik на использование HTTP-челленджа ACME (automatic certificate management environment) и заменяет опубликованные порты на 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

Оверлей также устанавливает ZITADEL_EXTERNALPORT: 443 и ZITADEL_EXTERNALSECURE: true для контейнера API, благодаря чему публичный URL и URL, которые Zitadel генерирует для себя, совпадают. A-запись должна разрешаться до запуска, иначе HTTP-челлендж завершится с ошибкой.

Если вы уже выполняете TLS-терминацию на nginx или балансировщике нагрузки, используйте docker-compose.mode-external-tls.yml и укажите в TRAEFIK_TRUSTED_IPS диапазоны адресов, с которых прокси отправляет запросы. Traefik принимает заголовки X-Forwarded-* только от адресов из этого списка, поэтому неверное значение приведет к отбрасыванию переданного протокола, и Zitadel начнет формировать http:// URL для HTTPS-сайта.

К вышестоящему (upstream) прокси Zitadel предъявляет два строгих требования. Он должен поддерживать HTTP/2 при работе с бэкендом, так как API использует gRPC. Также он должен передавать Host без изменений вместе с X-Forwarded-Proto: https. Пример конфигурации nginx от Zitadel демонстрирует структуру:

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;
    }
}

Имена upstream в примере соответствуют контейнерам в тестовой среде Zitadel, поэтому замените их на свои. Если вы предоставляете доступ к Zitadel через порт, отличный от 443, используйте grpc_set_header Host $host:$server_port;, чтобы номер порта передавался в заголовке. В остальном это обычный виртуальный хост, а разбор конфигурации nginx reverse proxy по строкам охватывает аспекты, не специфичные для Zitadel.

Первый администратор и принудительная смена пароля

При первом запуске создается один экземпляр, одна организация и один администратор. Имя для входа состоит из 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, поэтому укажите свои значения в небольшом оверлее. Назовите его 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 загружает docker-compose.override.yml автоматически только при запуске без флага -f, а каждая команда в руководстве 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 удерживает выполнение команды до тех пор, пока не пройдут проверки работоспособности (healthchecks). Если контейнер API не переходит в рабочее состояние, Compose останавливается с ошибкой dependency failed to start: container zitadel-compose-zitadel-api-1 is unhealthy, а причину можно найти в docker compose logs zitadel-api. При первом запуске причиной обычно является длина мастер-ключа или DSN базы данных.

Войдите в систему по адресу https://auth.example.com/ui/console, смените пароль и включите двухфакторную аутентификацию для этой учетной записи, прежде чем создавать что-либо еще. Все значения ZITADEL_FIRSTINSTANCE_* применяются только во время создания первого экземпляра. После того как экземпляр создан, их изменение ни на что не влияет.

Почему сброс пароля не работает без настроенного SMTP

Провайдер идентификации, который не может отправлять почту, неисправен, причем эта проблема может оставаться незамеченной неделями. Zitadel отправляет электронные письма для приглашения пользователей, подтверждения адресов, ссылок на сброс пароля, одноразовых кодов и уведомлений о правах на домен. Если SMTP-провайдер не настроен, консоль все равно сообщает о выполнении действия, а сообщение передается в очередь уведомлений, откуда его некуда отправить. По умолчанию для этого процесса заданы параметры MaxAttempts: 3 и MaxTtl: 5m, поэтому он выполняет несколько попыток в течение нескольких минут, а затем останавливается. Пользователь, ожидающий ссылку, не получает никаких уведомлений.

Настройте SMTP в консоли в разделе параметров экземпляра по адресу https://auth.example.com/ui/console/settings. Форма настройки SMTP-провайдера запрашивает адрес отправителя, имя отправителя, хост и порт, имя пользователя, пароль SMTP и переключатель TLS. Перед сохранением воспользуйтесь кнопкой тестирования в этой форме, так как она отправляет реальное сообщение: оно либо дойдет до адресата, либо нет.

Существует соответствующий набор переменных окружения, ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST и связанные с ними. Они применяются при создании экземпляра. В уже запущенном стеке они не имеют эффекта, поэтому для существующего экземпляра консоль является предпочтительным инструментом.

Два важных момента касательно отправки почты с VPS, так как именно здесь чаще всего возникают сбои. Большинство провайдеров блокируют исходящий порт 25 для новых учетных записей, поэтому прямая отправка на почтовый сервер получателя завершается по таймауту без полезного сообщения об ошибке. Используйте аутентифицированный релей на порту 587. Также опубликуйте записи SPF (sender policy framework) и DKIM (domainkeys identified mail) для домена отправителя, иначе ссылка на сброс пароля попадет в спам, что для пользователя выглядит так, будто письмо вообще не было отправлено.

Проверьте работу системы, прежде чем приглашать пользователей. Создайте временную учетную запись, запросите сброс пароля и убедитесь, что сообщение доставлено. Если этого не произошло, docker compose logs -f zitadel-api укажет на ошибку SMTP. Пароль SMTP хранится в базе данных в зашифрованном виде, что является еще одной задачей, которую выполняет для вас masterkey.

Раздельное резервное копирование PostgreSQL и 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 — это специальный формат, который сжимает данные при выгрузке и позволяет 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 запускается один раз, только в первый день. Поместите дамп и последние две команды в /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_test

Список таблиц в схеме eventstore подтверждает, что дамп корректен. Ошибка об отсутствии схемы означает, что дамп неисправен, и вы узнали об этом в день, когда это не повлекло за собой потерь. Общий шаблон резервного копирования и обновления стека Compose применим здесь почти без изменений, а хранение masterkey отдельно от архива — единственный нюанс, специфичный для Zitadel.

Обновление Zitadel без потери данных инстанса

Обновление представляет собой изменение версии в .env с последующим выполнением двух команд:

docker compose pull
docker compose up -d --wait

Разберитесь, что делает вторая команда, прежде чем запускать её в среде, где пользователи проходят аутентификацию. Команда контейнера — start-from-init, которая выполняет фазы инициализации и настройки перед началом обслуживания запросов; фаза настройки включает миграции базы данных. Таким образом, изменение версии запускает миграцию схемы в вашей рабочей базе данных при старте контейнера в автоматическом режиме, пока --wait ожидает прохождения проверки работоспособности (healthcheck). Именно поэтому тест восстановления, описанный выше, является обязательным.

Сделайте свежий дамп непосредственно перед обновлением. Дамп, сделанный вчера вечером, не является актуальным.

Не перескакивайте через мажорные версии. Переход с v3 на v4 требует предварительного обновления до версии v3.4.1 или более поздней, так как в v4 были удалены устаревшие ключи подписи OIDC, из-за чего токены, подписанные старыми ключами, перестанут проходить проверку сразу после обновления. В техническом бюллетене Zitadel A-10017 описано, что для исправления необходимо поработать на более новой версии v3 достаточно долго, чтобы старые токены успели истечь до выполнения обновления.

Контролируйте фазу настройки с помощью docker compose logs -f zitadel-api. Миграции в большом eventstore занимают минуты, а Traefik не будет направлять трафик на API, пока не пройдёт проверка работоспособности, поэтому сайт будет недоступен в течение этого времени. Планируйте это заранее, а не по факту возникновения простоя.

Откат к предыдущей версии — это не просто возврат старого тега. После выполнения миграций старый бинарный файл не сможет работать с изменённой схемой, поэтому откат означает восстановление из дампа. Как только в инстансе появятся реальные пользователи, переходите на docker-compose.prodlike.yml — оверлей, который выполняет инициализацию и настройку как отдельные шаги, независимые от запуска. В этом случае миграция становится процессом, который вы запускаете и контролируете вручную, а не побочным эффектом перезапуска контейнера.

Что указывать для нового поставщика идентификации

В консоли создайте проект, а затем приложение внутри него. Для любых современных решений выбирайте OIDC, и Zitadel предоставит вам client ID, client secret и документ обнаружения (discovery document) по адресу https://auth.example.com/.well-known/openid-configuration. Большинству self-hosted программ, поддерживающих единый вход (SSO), требуются именно эти данные.

Многие программы не поддерживают этот протокол или предоставляют его только в платных версиях. В первом случае oauth2-proxy перед приложением превращает любой HTTP-сервис в ресурс, который может защищать Zitadel. Во втором случае стоит прочитать статью налог на SSO в self-hosted приложениях, прежде чем планировать миграцию вокруг функциональности, за которую вы не заплатили.

FAQ

Сколько оперативной памяти и процессорных мощностей требуется для self-hosted Zitadel?

В руководстве по эксплуатации Zitadel рекомендуется использовать около 4 ядер процессора и 8 ГБ оперативной памяти для одного узла с минимальной конфигурацией, и 16 ГБ на узел при включенном логировании и сборе метрик. Ресурсы для PostgreSQL рассчитываются отдельно: примерно одно ядро на 100 запросов в секунду и 4 ГБ оперативной памяти на ядро. Быстрый старт через Docker Compose возможен в пределах 2 ГБ, чего достаточно для ознакомления, но это меньше рекомендуемых значений для системы, от которой зависят другие сервисы.

Что произойдет, если я потеряю мастер-ключ Zitadel?

Все данные, зашифрованные с его помощью, останутся зашифрованными. Секреты клиентов, учетные данные провайдеров идентификации, пароль SMTP и seed-значения для одноразовых паролей не подлежат расшифровке, а сам ключ невозможно изменить задним числом. Дамп базы данных сам по себе не позволяет восстановить работоспособный экземпляр, так как дамп содержит только зашифрованный текст без ключа. Храните мастер-ключ в менеджере паролей отдельно от резервной копии дампа. Если утеряны оба компонента, единственным выходом остается переустановка экземпляра с нуля.

Почему письма для сброса пароля Zitadel не приходят?

Это происходит, если SMTP-провайдер не настроен или настроен некорректно. Zitadel ставит каждое уведомление в очередь для воркера с тремя попытками по умолчанию и в любом случае сообщает об успехе в консоли, поэтому сбой происходит без уведомления. Настройте SMTP-провайдер в параметрах экземпляра и воспользуйтесь кнопкой тестирования в этой форме, которая отправляет реальное сообщение. При работе с VPS используйте аутентифицированный релей на порту 587, так как большинство провайдеров блокируют исходящий порт 25. Также опубликуйте SPF и DKIM записи для домена отправителя, чтобы письма не попадали в спам.

Можно ли изменить внешний домен Zitadel после установки?

Да, но недостаточно просто отредактировать .env. Измените ZITADEL_EXTERNALDOMAIN, ZITADEL_EXTERNALPORT и ZITADEL_EXTERNALSECURE, затем позвольте Zitadel повторно выполнить фазу настройки, чтобы изменения вступили в силу. Уже зарегистрированные приложения сохранят свои старые redirect URI, их придется обновлять вручную. Любой запрос, заголовок Host которого не совпадает с известным Zitadel доменом, получит ответ Instance not found. Выбор финального имени до первого запуска позволяет избежать всех этих сложностей.