ZITADEL на сервере: частые ошибки и решения
ZITADEL — один из немногих self-hosted SSO-проектов, изначально спроектированный как мультитенантная cloud-native платформа, а не адаптированный под контейнеры монолит: организации, проекты и инстансы — архитектурные сущности, а не надстройка поверх обычного OIDC-провайдера. Расплата за такую архитектуру — конфигурация чувствительнее к деталям: неверный ExternalDomain, недокрученный gRPC за reverse proxy или потерянный masterkey ломают вход жёстче, чем в Keycloak или Authentik. Разберём типовые ошибки установки ZITADEL на сервере и как их чинить без пересборки инстанса с нуля.
Содержание
- Setup выполнен не полностью — инстанс недоступен после первого запуска
- TLS и gRPC за reverse proxy — вход работает, а API падает
- ExternalDomain, ExternalPort и ExternalSecure — "invalid issuer" и петля редиректов
- Masterkey — потеря ключа шифрования данных
- SMTP-уведомления не отправляются
- Организации, проекты и кастомные домены — путаница мультитенантности
- Бэкап и обновление без потери настроек
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Setup выполнен не полностью — инстанс недоступен после первого запуска
После docker compose up -d контейнер zitadel в статусе running, но https://auth.example.com отдаёт 404 или "instance not found" — верный признак того, что миграции и первичная инициализация не отработали до конца.
ZITADEL разделяет запуск на фазы: инициализация базы (роли и схема), setup (миграции и создание первого инстанса, организации и админ-пользователя) и старт сервера. Для развёртывания с нуля обычно используют объединённую команду:
services:
zitadel:
image: ghcr.io/zitadel/zitadel:v2.63
command: 'start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled'
environment:
ZITADEL_DATABASE_POSTGRES_HOST: db
ZITADEL_DATABASE_POSTGRES_PORT: 5432
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${DB_PASSWORD}
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${DB_ADMIN_PASSWORD}
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
depends_on:
db:
condition: service_healthy
Повторный запуск start-from-init на уже проинициализированной базе не страшен — шаги setup идемпотентны, но лишний прогон миграций тратит время на старт. Смотрите логи на этапе первого поднятия:
docker compose logs -f zitadel | grep -iE "migration|setup|starting"
Учётные данные первого администратора либо задаются заранее файлом steps.yaml/firstinstance.yaml, смонтированным в контейнер, либо генерируются автоматически и печатаются в лог один раз при первом setup — если пропустили этот момент, восстановить пароль можно только через машинного пользователя с правами IAM_OWNER или прямой доступ к базе.
TLS и gRPC за reverse proxy — вход работает, а API падает
Классика: страница логина открывается, форма принимает пароль, а Console после входа виснет с ошибками вида rpc error: code = Unavailable или Failed to fetch. Причина почти всегда в том, что ZITADEL — в первую очередь gRPC-сервис, а HTTP-эндпоинты (Login UI, REST, gRPC-Web) — надстройка поверх него, и часть трафика требует полноценного HTTP/2 до бэкенда, а не просто TLS на входе.
Console и Login UI используют gRPC-Web, который работает поверх обычного HTTP/1.1 и через большинство reverse proxy проходит без проблем. А вот "чистый" gRPC — то, чем пользуются CLI, Terraform-провайдер и сервисные интеграции через API-ключи — требует сквозного HTTP/2 от клиента до контейнера. Если прокси перед ним даунгрейдит соединение до HTTP/1.1 (частая ситуация с nginx без явной настройки), веб-интерфейс работает, а автоматизация через API падает с невнятными сетевыми ошибками.
Для Traefik достаточно не мешать HTTP/2, который включён по умолчанию для HTTPS-роутеров:
labels:
- "traefik.http.routers.zitadel.rule=Host(`auth.example.com`)"
- "traefik.http.routers.zitadel.tls.certresolver=le"
- "traefik.http.services.zitadel.loadbalancer.server.port=8080"
- "traefik.http.services.zitadel.loadbalancer.server.scheme=h2c"
Схема h2c в лейбле важна отдельно: контейнер ZITADEL при --tlsMode disabled слушает обычный HTTP/2 без шифрования (h2c) на внутреннем порту, и Traefik должен обращаться к нему именно так, а не как к plain HTTP/1.1 бэкенду — иначе gRPC-запросы будут молча рваться на уровне прокси, хотя простые REST-вызовы отработают нормально.
С nginx сложнее: штатный proxy_pass не проксирует HTTP/2 до бэкенда без директивы grpc_pass, а её нужно применять только к gRPC-эндпоинтам, оставляя Login UI на обычном proxy_pass. Если переезжать на Traefik не хочется — закладывайте время на отдельный location под gRPC с grpc_pass и http2 на upstream заранее, а не после того, как интеграция через API откажется работать в проде.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверExternalDomain, ExternalPort и ExternalSecure — "invalid issuer" и петля редиректов
Если после входа браузер уходит в бесконечный редирект между auth.example.com и защищённым приложением, а в логах контейнера мелькает что-то про несовпадение issuer — почти всегда виноваты три переменные, которые обязаны отражать реальный публичный адрес инстанса, а не то, что видит контейнер изнутри сети:
ZITADEL_EXTERNALDOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=true
ZITADEL подписывает и валидирует токены с issuer, построенным именно из этих значений. Если ExternalPort указывает внутренний порт контейнера (например, 8080), а не 443, на котором пользователя видит прокси, issuer в токене не совпадёт с тем, что ожидает клиент при валидации — и OIDC-клиент, и сама Console откатят пользователя обратно на экран логина.
Отдельно проверьте, что X-Forwarded-Proto и X-Forwarded-Host доходят до контейнера от прокси в неизменном виде — по этим заголовкам ZITADEL понимает, что снаружи HTTPS. Без них ExternalSecure=true можно выставить, а Secure-флаг на cookie сессии всё равно не проставится, и браузер откажется её сохранять.
Если инстанс переехал на новый домен — одной переменной мало, домен нужно ещё явно добавить и подтвердить как кастомный домен инстанса через Console или Admin API, иначе новый адрес будет считаться сторонним для существующих OIDC-клиентов.
Masterkey — потеря ключа шифрования данных
ZITADEL_MASTERKEY — это не пароль администратора, а 32-символьный ключ, которым ZITADEL шифрует чувствительные данные прямо в PostgreSQL: секреты OIDC-клиентов, приватные ключи машинных пользователей, TOTP-сиды. Это единственная переменная во всём стеке, потерю которой нельзя компенсировать переустановкой пароля или доступом к базе напрямую.
Частая ошибка — сгенерировать masterkey один раз при деплое, не сохранить его нигде, кроме .env на диске сервера, а через полгода потерять этот файл при миграции стека. Контейнер снова запустится с новым ключом, но расшифровать существующие в базе секреты не сможет: клиентские секреты OIDC-приложений и API-ключи машинных пользователей придётся перевыпускать заново, а MFA-настройки на базе TOTP слетят и потребуют повторной привязки.
# сгенерировать валидный masterkey один раз и сохранить отдельно от сервера
openssl rand -base64 24 | cut -c1-32
Храните masterkey в менеджере секретов или хотя бы в зашифрованном бэкапе отдельно от диска с самим сервером — по сути это единственный "мастер-пароль" всей инсталляции, и его утрата эквивалентна частичной потере данных, даже если сама база физически цела.
SMTP-уведомления не отправляются
Приглашения, письма подтверждения почты и восстановления пароля не доходят, а в Console явной ошибки не видно — доставка идёт асинхронно через внутренние воркеры-нотификаторы, и сбой не всегда всплывает в интерфейсе сразу.
SMTP настраивается либо через переменные окружения на старте, либо позже через Console → Instance → Settings → Notification Providers — второй способ удобнее, потому что не требует пересоздания контейнера при смене пароля почтового ящика:
ZITADEL_SMTP_HOST=smtp.example.com:587
ZITADEL_SMTP_USER=noreply@example.com
ZITADEL_SMTP_PASSWORD=app-specific-password
ZITADEL_SMTP_TLS=true
ZITADEL_EMAILVERIFICATION_SMTP_FROM=noreply@example.com
Если провайдер настроен, а письма всё равно не уходят — смотрите логи именно на уровне notification-компонента:
docker compose logs zitadel | grep -iE "notification|smtp|mail"
Типичные причины: перепутанный порт (587 требует STARTTLS, 465 — implicit TLS, это разные флаги), пароль от основного аккаунта вместо пароля приложения у Gmail/Yandex, и отсутствующие SPF/DKIM записи на отправляющем домене — письмо ZITADEL честно отправляет, но почтовый сервер получателя тихо роняет его в спам. Это уже зона ответственности DNS, а не платформы.
Организации, проекты и кастомные домены — путаница мультитенантности
ZITADEL строит иерархию инстанс → организация → проект → приложение, и если вы пришли с опытом Keycloak или Authentik, первая практическая проблема — не туда завести пользователей или клиента.
Каждый инстанс при первичном setup получает одну дефолтную организацию. Если создать вторую организацию для отдельного клиента и завести туда пользователей, а OIDC-приложение по ошибке настроить в первой (дефолтной) организации — авторизация будет молча падать с "user not found", хотя пользователь точно существует, просто в другом тенанте. Это осознанное поведение платформы: организации в ZITADEL — полноценные изолированные пространства, а не просто теги.
Второй источник путаницы — кастомные домены на уровне организации (не путать с ExternalDomain инстанса из раздела выше). Если у организации должен быть свой логин-домен вида login.client-company.com, его нужно явно добавить в Console → Organization → Domains и подтвердить владение через DNS TXT-запись — только после этого он начнёт участвовать в discovery для входа; просто прописать A-запись на IP сервера недостаточно.
Для команд, которые до ZITADEL смотрели в сторону Keycloak как более "тяжёлого" промышленного стандарта, разница в модели мультитенантности часто становится решающим фактором выбора — сравнение подходов есть в статье Keycloak на сервере: частые ошибки и решения.
Бэкап и обновление без потери настроек
Вся конфигурация ZITADEL — организации, проекты, политики, пользователи, event-журнал изменений — хранится в PostgreSQL в виде event-sourced модели. Бэкапить нужно именно базу целиком (плюс masterkey и стартовые переменные окружения отдельно):
docker compose exec -T db pg_dump -U zitadel -Fc zitadel > zitadel_$(date +%F).dump
# восстановление на чистую базу
docker compose exec -T db pg_restore -U zitadel -d zitadel --clean --if-exists < zitadel_2026-08-20.dump
Перед обновлением читайте changelog в репозитории проекта — мажорные релизы иногда несут breaking changes в схеме событий или API. Общий порядок обновления в docker compose:
docker compose pull zitadel
docker compose stop zitadel
# правим тег образа в docker-compose.yml
docker compose up -d zitadel
docker compose logs -f zitadel | grep -iE "migration|error"
Не запускайте start-from-init на уже работающей базе с продовыми данными без крайней необходимости — используйте обычный start (или start-from-setup, если нужно прогнать только недостающие миграции). Держите минимум пару последних дампов вне сервера: без базы восстановить работоспособный инстанс нельзя в принципе.
Общая логика проксирования HTTP/2 и работы с TLS-сертификатами для reverse proxy подробно разобрана в статье Traefik на сервере: частые ошибки и решения, а раз вся платформа держится на PostgreSQL — стоит заранее свериться с типовыми проблемами самой базы в статье PostgreSQL на сервере: частые ошибки и решения, многие "странности" ZITADEL на практике оказываются проблемами СУБД, а не самого identity-провайдера.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Обязательно ли использовать PostgreSQL?
Да, это единственная официально поддерживаемая продовая СУБД для актуальных версий ZITADEL, отдельного "лёгкого" режима на SQLite для продакшена нет.
Можно ли запустить без TLS вообще, для внутреннего теста?
Технически да, с --tlsMode disabled внутри закрытой сети — но ExternalSecure всё равно нужно выставлять по факту того, что видит пользователь, иначе куки и redirect URI соберутся неправильно.
Сколько ресурсов нужно для небольшой команды?
Комфортный старт — от 1-2 vCPU и 2 ГБ RAM под сам ZITADEL плюс отдельные ресурсы под PostgreSQL; точные цифры зависят от числа организаций и частоты авторизаций, ориентируйтесь на нагрузочное тестирование под свой профиль.
Что делать, если забыли пароль первого администратора?
Через машинного пользователя с ролью IAM_OWNER и его Personal Access Token пароль можно сбросить через Management API без доступа к базе; без такого пользователя единственный путь — правка данных напрямую в PostgreSQL, что без опыта работы с внутренней схемой не рекомендуется.
Actions (скрипты на события) — стабильная фича?
Возможность вешать код на события flow (создание пользователя, до/после выдачи токена) есть, но синтаксис и API менялись между версиями — сверяйтесь с changelog перед тем как завязывать на них критичную логику.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →