MAATRIX / Блог / ZITADEL в Docker Compose: готовый файл

ZITADEL в Docker Compose: готовый файл

MAATRIX

Если вы выбираете между Auth0/Okta и self-hosted решением, ZITADEL — один из немногих вариантов, который реально закрывает мультитенантность из коробки: организации, проекты и приложения внутри одного инстанса, без костылей вроде отдельного realm на каждого клиента, как в Keycloak. Расплата — более сложный запуск: ZITADEL написан на Go с gRPC-ядром, требует HTTP/2 до самого себя и не терпит "просто подниму без TLS и разберусь потом". Ниже — рабочий docker-compose.yml, который заводится с первого раза, и объяснение, почему без reverse proxy с TLS-терминацией тут не обойтись.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое ZITADEL и когда он вам нужен

ZITADEL — cloud-native identity и access management платформа: OAuth2/OIDC, SAML, SCIM для провижининга, passkeys/WebAuthn, действия (Actions) на TypeScript для кастомной логики при логине. Отличие от Keycloak и Authentik — в модели данных: у ZITADEL есть верхнеуровневые Organizations, а внутри каждой — свои проекты, приложения, пользователи и роли. Это ровно то, что нужно, если вы строите SaaS и каждому клиенту нужен изолированный контур авторизации без развёртывания отдельного сервиса на клиента.

Если вам нужен просто SSO для внутренних сервисов компании — присмотритесь сначала к более лёгким вариантам, например Authentik или Authelia: у них ниже порог входа и меньше требований к инфраструктуре. ZITADEL оправдан, когда мультитенантность — не опция, а требование продукта, либо когда важен event-sourcing подход к данным (вся история изменений хранится, а не перезаписывается).

По требованиям к железу: минимум 2 vCPU и 4 ГБ RAM для тестового контура, для продакшена с полноценной нагрузкой закладывайте 4 vCPU / 8 ГБ и отдельный диск под PostgreSQL — event store у ZITADEL растёт быстрее, чем кажется на старте, за счёт хранения полной истории событий.

Архитектура связки

Минимальная рабочая схема — три компонента:

  • PostgreSQL 16+ — основное и единственное официально поддерживаемое хранилище с версии 2.x (CockroachDB тоже поддерживается, но для self-hosted проще взять Postgres);
  • ZITADEL — сам сервис, слушает gRPC/HTTP2 на одном порту;
  • Reverse proxy с TLS (Traefik или Caddy) — обязателен, потому что клиенты ZITADEL (консоль, gRPC-клиенты) ожидают HTTP/2 по HTTPS. Без TLS-терминации перед сервисом часть функций консоли просто не заработает в браузере.

Опционально добавляют SMTP-relay для писем верификации и восстановления пароля — можно использовать внешний сервис (Mailgun, Postmark) или свой SMTP.

Все три компонента разворачиваются на одном сервере без проблем — ZITADEL не требователен к сети между компонентами, важна только связка proxy → zitadel по HTTP/2.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Готовый docker-compose.yml

Структура каталога:

zitadel/
├── docker-compose.yml
├── .env
└── machinekey/        # сюда ZITADEL положит ключ сервисного аккаунта

Файл .env:

# Домен, на который смотрит reverse proxy
ZITADEL_DOMAIN=auth.example.com

# Пароль для служебного пользователя Postgres
POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD

# 32-байтовый ключ шифрования — сгенерировать один раз и больше не терять,
# без него не расшифруются секреты в базе
ZITADEL_MASTERKEY=CHANGE_ME_32_BYTE_KEY_BASE64

Мастер-ключ генерируется так:

openssl rand -base64 32 | tr -d '\n' | head -c 32; echo

Сам docker-compose.yml:

version: "3.8"

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: zitadel
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: zitadel
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U zitadel -d zitadel"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - zitadel_net

  zitadel:
    image: ghcr.io/zitadel/zitadel:latest
    restart: unless-stopped
    command: start-from-init --masterkeyFromEnv --tlsMode external
    depends_on:
      db:
        condition: service_healthy
    environment:
      ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY}
      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: ${POSTGRES_PASSWORD}
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: zitadel
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${POSTGRES_PASSWORD}
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
      ZITADEL_EXTERNALSECURE: "true"
      ZITADEL_EXTERNALDOMAIN: ${ZITADEL_DOMAIN}
      ZITADEL_EXTERNALPORT: 443
      ZITADEL_TLS_ENABLED: "false"
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: admin
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./machinekey:/machinekey
    networks:
      - zitadel_net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.zitadel.rule=Host(`${ZITADEL_DOMAIN}`)"
      - "traefik.http.routers.zitadel.entrypoints=websecure"
      - "traefik.http.routers.zitadel.tls.certresolver=le"
      - "traefik.http.services.zitadel.loadbalancer.server.port=8080"
      - "traefik.http.services.zitadel.loadbalancer.server.scheme=h2c"

networks:
  zitadel_net:
    external: true
    name: traefik_net

volumes:
  db_data:

Обратите внимание на tlsMode external и ZITADEL_TLS_ENABLED: "false" — сам ZITADEL TLS не терминирует, это делает proxy перед ним. При этом ZITADEL_EXTERNALSECURE: "true" говорит сервису, что снаружи он доступен по HTTPS — иначе редиректы и cookies будут собираться неправильно.

Тег latest в проде лучше не использовать вслепую — зафиксируйте конкретную версию из релизов ZITADEL на GitHub и обновляйте осознанно, читая changelog: между мажорными версиями бывают breaking changes в схеме БД.

TLS и reverse proxy: почему это не опционально

ZITADEL Console (веб-интерфейс администрирования) и часть SDK общаются через gRPC-Web, для которого браузеру нужен HTTP/2. Если у вас уже поднят Traefik как reverse proxy для Docker, лейблов из compose-файла выше достаточно — важна только строка scheme=h2c, без неё Traefik будет ходить к ZITADEL по HTTP/1.1 и часть запросов от консоли будет падать с непонятными ошибками в консоли браузера.

Если вы ставите с нуля, сначала разверните сам Traefik с автоматическим Let's Encrypt — по шагам это описано в статье про установку Caddy с авто-SSL, логика с ACME-резолвером в Traefik аналогична. Caddy тоже подходит, но конфиг для h2c-проксирования на upstream там придётся прописывать в Caddyfile вручную через reverse_proxy … { transport http { versions h2c } }.

Проверить, что HTTP/2 реально работает после запуска:

curl -I --http2 https://auth.example.com/debug/healthz

Ответ HTTP/2 200 подтверждает, что цепочка proxy → zitadel настроена верно.

Первый запуск и создание администратора

Поднимаем стек:

docker network create traefik_net   # если сети ещё нет
docker compose up -d
docker compose logs -f zitadel

Команда start-from-init при первом запуске сама прогонит миграции схемы в PostgreSQL и создаст первый инстанс с организацией по умолчанию и пользователем-администратором из переменных ZITADEL_FIRSTINSTANCE_ORG_HUMAN_*. Это происходит один раз — при последующих перезапусках контейнера команду можно (и нужно) сменить на start, иначе при каждом рестарте будет попытка повторной инициализации.

После первого успешного старта зайдите на https://auth.example.com/ui/console, войдите под admin и паролем из .env, и первым делом:

  1. смените пароль администратора;
  2. включите двухфакторную аутентификацию для этого аккаунта;
  3. создайте отдельного machine-пользователя (сервисный аккаунт) для API-интеграций вместо использования human-аккаунта в скриптах.

Мультитенантность: организации, проекты, приложения

Здесь ZITADEL раскрывается там, где Keycloak и Authentik требуют обхода. Иерархия такая:

  • Instance — весь развёрнутый инстанс ZITADEL (в docker-compose выше — один);
  • Organization — изолированный тенант: свои пользователи, свои настройки логина, свой брендинг страницы входа;
  • Project — приложение или группа приложений внутри организации, с ролями (RBAC) и грантами;
  • Application — конкретный OIDC/SAML/API клиент с секретами и redirect URI.

На практике для SaaS-продукта это значит: заводите отдельную Organization на каждого клиента-компанию (или используете один Project с грантами на разные организации, если удобнее делиться приложением между тенантами). Роли назначаются на уровне Project и затем предоставляются (grant) конкретной организации — так один и тот же продукт может быть переиспользован между клиентами без дублирования конфигурации OIDC-клиента.

Для интеграции по API (Terraform-провайдер ZITADEL, автоматическое создание организаций при регистрации нового клиента) используйте machine-пользователя с ключом из каталога ./machinekey — он появится там автоматически после start-from-init, если добавить в compose переменную ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH: /machinekey/zitadel-admin-sa.json.

Бэкап, обновления и мониторинг

Все данные ZITADEL живут в PostgreSQL — бэкапить нужно именно базу, сам контейнер zitadel stateless (кроме каталога machinekey с ключами сервисных аккаунтов, его тоже сохраняйте).

docker compose exec db pg_dump -U zitadel -Fc zitadel > zitadel_$(date +%F).dump

Восстановление:

docker compose exec -T db pg_restore -U zitadel -d zitadel --clean < zitadel_2026-08-20.dump

Настройте это через cron или через готовый контейнер restic для инкрементальных бэкапов с шифрованием и выгрузкой в S3-совместимое хранилище — так вы не потеряете дамп вместе с сервером при аварии.

Перед обновлением мажорной версии обязательно читайте changelog релиза на предмет миграций схемы — event-sourcing модель ZITADEL хранит историю событий, и откат на предыдущую версию после неудачной миграции не всегда тривиален. Практика, которая себя оправдывает: снять дамп базы прямо перед обновлением, обновить на staging-копии сервера первым, и только потом катить на прод.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли обойтись без reverse proxy и TLS, хотя бы для теста?

Технически ZITADEL запустится и с tlsMode disabled без TLS вообще, но консоль администратора будет частично неработоспособна из-за требований к HTTP/2 в браузере, а логин через OIDC у многих клиентских библиотек требует HTTPS redirect URI. Для чего-то серьёзнее локального смоук-теста TLS нужен сразу.

Чем ZITADEL отличается от Keycloak на практике?

Keycloak старше, у него больше готовых интеграций и community-модулей, но мультитенантность там эмулируется через отдельные realm, которые не умеют шарить проекты между собой. В ZITADEL организации и гранты проектов между ними — часть модели данных с самого начала, это удобнее для SaaS с десятками и сотнями тенантов. Если хотите сравнить детальнее, у нас есть отдельный разбор Keycloak в Docker Compose.

Поддерживает ли ZITADEL SCIM для синхронизации пользователей?

Да, ZITADEL реализует SCIM 2.0 для автоматического провижининга и депровижининга пользователей из внешних систем (например, из корпоративного Google Workspace или Azure AD) — конфигурируется на уровне организации.

Сколько RAM реально нужно под продакшен?

Точная цифра зависит от количества активных сессий и частоты логинов, но как ориентир: связка ZITADEL + Postgres на пару тысяч активных пользователей комфортно работает на 8 ГБ RAM с запасом. Под серьёзную нагрузку тестируйте на своём трафике — event store растёт быстрее плоских таблиц классических IdP.

Что делать, если забыли ZITADEL_MASTERKEY?

Ничего — без него не расшифровать секреты клиентов OIDC и другие зашифрованные поля в базе. Ключ нужно хранить отдельно от сервера (менеджер секретов, зашифрованный бэкап), восстановить утерянный masterkey невозможно, только пересоздавать секреты приложений заново.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →