> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Быстрый старт: self-hosted

> Запуск Veriqa как отдельного OIDC-сервера в Docker — ваше приложение остаётся обычным OIDC-клиентом.

В self-hosted-режиме Veriqa работает как **собственный процесс**: standalone OIDC-сервер
аутентификации в контейнере (внутри — OpenIddict, но вашему приложению это знать не нужно). Ваше
приложение общается с ним по стандартному OpenID Connect и не ссылается ни на один пакет Veriqa.

<Note>
  **Путь, удобный для compliance.** Поскольку ваше приложение — обычный OIDC-клиент, ни один
  компонент Veriqa не попадает в вашу сборку и SBOM. Если в организации сканеры лицензий проходят
  по зависимостям — выбирайте этот режим: вы интегрируетесь по протоколу, а не по пакету.
  Встроенная альтернатива — [.NET quickstart](/docs/ru/quickstart/dotnet).
</Note>

## 1. Получите образ

Каждый релиз публикуется в реестр контейнеров публичного репозитория:

```bash theme={null}
docker pull registry.gitlab.com/veriqa/veriqa
```

Без тега Docker берёт `latest` — самый свежий релиз. Чтобы закрепить релиз в продакшене, укажите
его версию тегом (`registry.gitlab.com/veriqa/veriqa:<версия>`).

<Note>
  Опубликованный образ — `linux/amd64`. На другой архитектуре соберите его из корня репозитория —
  `Dockerfile` там публикует тот же standalone-хост — и подставьте свой локальный тег вместо имени
  образа в командах ниже:

  ```bash theme={null}
  docker build -t veriqa-authserver .
  ```
</Note>

Образ многостадийный (сборка на SDK → рантайм ASP.NET), запускается под **non-root** пользователем
(UID `1654`), экспонирует порт **8080** по HTTP и несёт `HEALTHCHECK` против `/health/live`. TLS
ожидается терминированным перед ним — см. [шаг 6](#6-поставьте-за-reverse-proxy).

## 2. Настройте его

Контейнер читает ту же конфигурацию, что и любое ASP.NET Core приложение, поэтому каждая настройка
может быть передана переменной окружения. Вложенные ключи используют **двойное подчёркивание** как
разделитель: `Veriqa:OpenIddict:Database:Provider` превращается в
`Veriqa__OpenIddict__Database__Provider`.

```bash docker run theme={null}
docker run -d --name veriqa \
  -p 8080:8080 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e Veriqa__OpenIddict__Database__Provider=PostgreSQL \
  -e Veriqa__OpenIddict__Database__ConnectionString="Host=postgres;Database=veriqa;Username=veriqa;Password=…" \
  -e Veriqa__TransactionEngine__Store__Provider=PostgreSQL \
  -e Veriqa__TransactionEngine__Store__ConnectionString="Host=postgres;Database=veriqa;Username=veriqa;Password=…" \
  -e Veriqa__Channels__Telegram__Enabled=true \
  -e Veriqa__Channels__Telegram__BotToken="YOUR_BOT_TOKEN" \
  -e Veriqa__Channels__Telegram__WebhookBaseUrl="https://auth.your-domain.com" \
  -e Veriqa__Channels__Telegram__WebhookSecretToken="YOUR_SECRET" \
  -e Veriqa__Channels__Telegram__InboundVerification=shared_secret \
  registry.gitlab.com/veriqa/veriqa
```

<Warning>
  Секреты в командной строке попадают в историю оболочки и в `docker inspect`. Используйте
  `--env-file`, секреты Docker/Kubernetes или ваш secret-manager — пример выше расписан лишь для
  показа имён ключей.
</Warning>

Хранилища настраиваются независимо и могут указывать на одну и ту же БД:

* **`Veriqa:OpenIddict:Database`** — OIDC-клиенты, авторизации и токены. **Умолчания нет:** контейнер
  с пустой секцией не стартует — проверка на старте отказывает и называет лечение (в `Development`
  тот же текст пишется как `Warning`). Для реального использования задайте `Provider` = `PostgreSQL`
  или `SqlServer` и укажите `ConnectionString`; `Provider` = `InMemory` — это явный выбор волатильного
  хранилища: каждый рестарт сбрасывает выданные токены — подходит для разработки и для
  [коннектора на одном инстансе](/docs/ru/guides/storage#коннектор-на-одном-инстансе-без-базы-данных).
* **`Veriqa:TransactionEngine:Store`** — сами транзакции входа. Задание `ConnectionString`
  переключает его на EF Core; пустое значение оставляет хранилище в памяти. `Provider` —
  `PostgreSQL` (он же при пустом ключе) или `SqlServer`.
* **`Veriqa:Logging:Store`** — журнал аудита, необязательно: по умолчанию он делит БД с
  транзакциями. См. [Сток аудита](#сток-аудита).

Значения провайдера регистр не учитывают. Для SQL Server строка подключения — в обычной форме
SqlClient: `Server=sqlserver;Database=veriqa;User Id=veriqa;Password=…`.

Резолверы — **fail-fast**: нераспознанное значение провайдера или реляционный провайдер без
строки подключения останавливают контейнер на старте, а не молча падают в фолбэк.

### Сток аудита

Записи аудита пишутся только при `Veriqa:Logging:Mode` = `Audit`. Куда они попадают, решают два
необязательных ключа, и каждый откатывается на своё значение независимо:

| Ключ | Если не задан |
| - | - |
| `Veriqa:Logging:Store:ConnectionString` | берётся `Veriqa:TransactionEngine:Store:ConnectionString` |
| `Veriqa:Logging:Store:Provider` | берётся `Veriqa:TransactionEngine:Store:Provider`; пусто и там — `PostgreSQL` |

Оставьте оба незаданными — журнал живёт в БД транзакций. Отдельная БД нужна, когда:

* учётной записью, под которой хранятся записи, должен владеть аудитор — отдельно от той, которой
  пишет путь входа;
* профили нагрузки и бэкапа разные: транзакции исчезают за минуты, записи аудита живут
  `RetentionDays`;
* транзакции остаются в памяти, а журнал должен пережить рестарт — задайте только
  `Veriqa:Logging:Store:ConnectionString`.

Собственный провайдер поверх заимствованной строки подключения отклоняется на старте сообщением,
называющим оба ключа `Provider`: задайте и строку стока, либо уберите провайдер. Если строка стока
отличается от строки транзакций, у readiness появляется отдельная проверка (`postgresql-audit` или
`sqlserver-audit`).

**Существующая установка.** У установки, работавшей без этих ключей, `AuditRecords` лежат в БД
транзакций. Задание `Veriqa:Logging:Store:ConnectionString` их не переносит: новая БД начинает с
пустой таблицы, а оставшиеся строки ретенция больше не вычищает. Перенесите их или удалите в рамках
переключения.

**Режиму `Audit` нужен долговечный сток.** При `Veriqa:Logging:Mode` = `Audit` и незаданных обеих
строках подключения записи хранились бы в памяти и терялись при рестарте — поэтому вне `Development`
контейнер отказывается стартовать:

```
Veriqa:Logging:Mode is 'Audit', but the audit store is in memory: set 'Veriqa:Logging:Store:ConnectionString' or 'Veriqa:TransactionEngine:Store:ConnectionString'.
```

В `Development` тот же текст пишется как `Warning`. Проверка выполняется на старте: переключение в
`Audit` позже, без рестарта, повторно не проверяется.

## 3. Предоставьте сертификаты токенов

Вне `Development` сервер отказывается стартовать без **обоих** сертификатов OpenIddict — подписи и
шифрования: авто-генерируемых dev-ключей, на которые можно откатиться, нет. Передайте каждый либо
путём к файлу, либо инлайн как Base64 PFX.

Подойдёт любой сертификат RSA-2048 (или сильнее), самоподписанного достаточно. Один из способов
выпустить оба через OpenSSL:

```bash theme={null}
for name in signing encryption; do
  openssl req -x509 -newkey rsa:2048 -nodes -days 730 -subj "/CN=veriqa-$name" \
    -keyout "$name.key" -out "$name.crt"
  openssl pkcs12 -export -inkey "$name.key" -in "$name.crt" -out "$name.pfx" \
    -passout env:PFX_PASSWORD
done
```

Переменная `PFX_PASSWORD` хранит пароль, который затем передаётся как `…CertificatePassword`. Файлы
`.key` не кладите ни в образ, ни в репозиторий. Оба файла `.pfx` `openssl` создаёт с правами
`-rw-------`, владельцем становится запустивший его пользователь — это важно, как только вы
монтируете их в контейнер, см. предупреждение ниже.

```bash theme={null}
-e Veriqa__OpenIddict__Server__SigningCertificateBase64="$(base64 -w0 signing.pfx)" \
-e Veriqa__OpenIddict__Server__SigningCertificatePassword="…" \
-e Veriqa__OpenIddict__Server__EncryptionCertificateBase64="$(base64 -w0 encryption.pfx)" \
-e Veriqa__OpenIddict__Server__EncryptionCertificatePassword="…"
```

Варианты `…Path` (`SigningCertificatePath` / `EncryptionCertificatePath`) принимают путь к
PFX, смонтированному в контейнер, — обычно вариант лучше, так как Base64-форма даёт очень длинные
значения переменных окружения.

<Warning>
  **Смонтированный PFX должен быть читаем пользователем контейнера.** Образ работает под UID `1654`
  ([шаг 1](#1-получите-образ)), а файлы, которые даёт рецепт выше, читает только выпустивший их
  пользователь хоста. Смонтированные как есть, они валят контейнер на старте с
  `CryptographicException: Error occurred during a cryptographic operation` — сообщением, которое не
  называет ни путь, ни отказ в доступе. Перед монтированием передайте файлы пользователю образа:

  ```bash theme={null}
  sudo chown 1654 signing.pfx encryption.pfx
  ```

  `chown` — лучший из двух способов: файлы остаются закрыты для всех, кроме пользователя образа.
  Но он требует root на хосте — там, где его нет, читать файлы контейнеру даст и
  `chmod 644 signing.pfx encryption.pfx`, только тогда PFX становится читаем **любому** локальному
  пользователю, а пароль, которым он открывается, лежит рядом — в окружении контейнера. Применяйте
  `chmod` только на хосте, который принадлежит вам одному. Файлы `.key` в контейнер не монтируются
  вовсе, а Base64-формы это не касается — там сертификат едет значением переменной окружения, а не
  файлом.
</Warning>

<Warning>
  Этими сертификатами подписываются и шифруются ваши выданные токены. Держите их стабильными между
  рестартами и общими между инстансами — их ротация инвалидирует все живые токены — и относитесь к
  паролям как к секретам.
</Warning>

## 4. Объявите свой клиент

Приложение, которое выполняет вход пользователей, регистрируется как OIDC-клиент в
`Veriqa:OpenIddict:Clients`. Образ **не** несёт клиентов, поэтому этот шаг обязателен — и каждый
клиент валидируется на старте: отсутствие `ClientId`, пустой `AllowedRedirectUris` или пустой
`AllowedScopes` останавливают контейнер с сообщением, называющим проблемную запись.

С env-файлом это удобнее выразить как JSON — смонтируйте файл настроек `veriqa.json` в контейнер по
пути `/app/config/veriqa.json` (переменная `VERIQA_SETTINGS_FILE` переносит его в другое место; если по заданному в ней пути файла нет, контейнер не стартует). В файле
допустимы любые настройки Veriqa, не только клиенты; ключ, заданный через окружение или `--env-file`,
перекрывает тот же ключ из файла:

```json veriqa.json theme={null}
{
  "Veriqa": {
    "OpenIddict": {
      "Clients": [
        {
          "ClientId": "my-app",
          "DisplayName": "My Application",
          "AllowedRedirectUris": [ "https://app.your-domain.com/signin-oidc" ],
          "AllowedScopes": [ "openid", "profile", "offline_access" ],
          "AllowRefreshTokens": true
        }
      ]
    }
  }
}
```

```bash theme={null}
docker run -d --name veriqa -p 8080:8080 \
  -v "$(pwd)/veriqa.json:/app/config/veriqa.json:ro" \
  --env-file veriqa.env \
  registry.gitlab.com/veriqa/veriqa
```

`redirect_uri` сверяется **точным** совпадением с `AllowedRedirectUris` — wildcard'ы не
поддерживаются, URI должны быть абсолютными, а `http` принимается только для loopback-адресов
(`localhost`, `127.0.0.1`, `[::1]`) в разработке.

## 5. Направьте своё приложение на него

Ваше приложение — стандартный OIDC-клиент. Ничего специфичного для Veriqa в него не попадает — это
та же конфигурация, что вы написали бы для любого OpenID Connect провайдера. В ASP.NET Core она
приезжает одним пакетом:

```bash theme={null}
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
```

```csharp Program.cs (ваше приложение, не Veriqa) theme={null}
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.Authority = "https://auth.your-domain.com";
        options.ClientId = "my-app";
        options.ResponseType = "code";
        options.Scope.Add("profile");
        options.CallbackPath = "/signin-oidc";

        // Veriqa выдаёт имя пользователя коротким OIDC-claim `name`, а User.Identity.Name по
        // умолчанию ищет ClaimTypes.Name (длинный URI WS-Federation) — без этой строки имя после
        // успешного входа останется пустым.
        options.TokenValidationParameters.NameClaimType = "name";
    });
```

Аватар пользователя (`picture`) гейтится Veriqa-специфичным scope `avatar`: добавьте
`options.Scope.Add("avatar")` и `options.GetClaimsFromUserInfoEndpoint = true`, а `avatar` впишите в
`AllowedScopes` клиента из [шага 4](#4-объявите-свой-клиент). В `id_token` аватар не попадает никогда —
см. [OIDC + Veriqa: разбор](/docs/ru/concepts/oidc-explainer#resolved-identity-и-claims).

Discovery, authorization code flow и выдача токенов — обычное поведение OpenIddict, поэтому подходит
любая совместимая OIDC-клиентская библиотека — .NET, Node, Go, Java, Python.

Блок выше — только регистрация. Чтобы провести вход из [шага 8](#8-проверьте), нужны ещё конвейер
(`app.UseAuthentication()`, `app.UseAuthorization()`) и конечная точка, выдающая challenge, —
полная программа клиента, вместе с ними обоими, лежит в
[шаге 5 .NET quickstart](/docs/ru/quickstart/dotnet#5-подключите-клиентское-приложение). Та программа
написана для встроенного quickstart, поэтому два значения в ней свои: `Authority` указывает на
`https://localhost:7300`, и она запрашивает scope `email`, которого нет в `AllowedScopes` клиента
из [шага 4](#4-объявите-свой-клиент) — либо уберите `options.Scope.Add("email")`, либо добавьте
`email` в `AllowedScopes`. Остальное применимо как есть.

## 6. Поставьте за reverse-proxy

Контейнер говорит по HTTP; TLS терминируется на вашем прокси. Чтобы OpenIddict строил корректные
`https`-URL, прокси должен пробрасывать `X-Forwarded-Proto` / `X-Forwarded-For`, а хосту нужно
сообщить, каким прокси доверять, — в том же `veriqa.json` из [шага 4](#4-объявите-свой-клиент):

```json veriqa.json theme={null}
{
  "ForwardedHeaders": {
    "KnownProxies": [ "10.0.0.5" ],
    "KnownIPNetworks": [ "10.0.0.0/8" ]
  }
}
```

В контейнере это не опционально. По умолчанию доверяется только loopback, а запрос, приходящий
через контейнерную сеть, идёт с gateway моста — так что ненастроенный хост молча игнорирует
`X-Forwarded-Proto`, продолжает считать запрос `http`, и даже
`/.well-known/openid-configuration` отвечает `invalid_request` («This server only accepts HTTPS
requests»). Как только сеть прокси перечислена, discovery возвращает `https`-URL и вход работает.

Оставьте прокси запас и на заголовки запроса. Запросы входа несут cookie: собственные cookie Veriqa
и, если ваше приложение делит с ней имя хоста или родительский домен (в разработке все порты
`localhost` — один хост), cookie корреляции, nonce и сессии OIDC-клиента. NGINX по умолчанию
ограничивает один заголовок 8k и на более длинный отвечает `400 Request Header Or Cookie Too Large`
— поднимите лимит в блоке `server`:

```nginx theme={null}
large_client_header_buffers 4 32k;
```

Прокси — это и место per-IP rate limiting. Ядро считает свои лимиты на маршрут, на пользователя и
на клиента, но не на адрес, поэтому ограничитель по IP клиента (`limit_req` в NGINX, правило WAF,
fail2ban) — часть этого же шага, см.
[Hardening к продакшну](/docs/ru/guides/hardening#rate-limiting-и-anti-abuse).

## 7. Включите каналы

Настройка каналов — токены ботов, регистрация вебхуков, Meta Cloud API, SMTP — идентична в обоих
режимах поставки и живёт в [Настройке каналов](/docs/ru/guides/channels). Одна деталь специфична для
self-hosting: вебхуки каналов должны быть доступны **из интернета** по
`https://auth.your-domain.com/api/channels/…`, поэтому reverse-proxy обязан выставить эти пути.

Хотя бы один канал обязателен — без него вход подтверждать негде. В команде из
[шага 2](#2-настройте-его) включён Telegram; вот что значат его ключи, чтобы дойти до рабочего
входа, не уходя с этой страницы:

| Ключ | Обязателен | Зачем |
| - | - | - |
| `Veriqa__Channels__Telegram__Enabled` | да | Включает адаптер. Регистрировать канал в коде не нужно — образ несёт все четыре, конфигурация решает, какие из них живут |
| `Veriqa__Channels__Telegram__BotToken` | да | Токен бота, созданного через [@BotFather](https://t.me/BotFather) |
| `Veriqa__Channels__Telegram__UpdateMode` | нет | `Webhook` (по умолчанию) или `Polling` |
| `Veriqa__Channels__Telegram__WebhookBaseUrl` | в режиме `Webhook` | Публичный HTTPS base URL этого хоста |
| `Veriqa__Channels__Telegram__WebhookSecretToken` | в режиме `Webhook` | Произвольная случайная строка; ею аутентифицируется каждый входящий update |
| `Veriqa__Channels__Telegram__InboundVerification` | да | Как проверяется подлинность входящего события канала: `shared_secret` в режиме `Webhook`, `outbound_fetch` в режиме `Polling`. Включённый канал без этого ключа не даст хосту стартовать; словарь и значения по остальным каналам — в [Настройке каналов](/docs/ru/guides/channels) |
| `Veriqa__Channels__Telegram__BotUsername` | нет | Экономит обращение к Bot API: задан — `t.me`-deep-link за QR собирается сразу, пуст — имя бота резолвится запросом к API. На приём апдейтов не влияет |

Вебхук Veriqa регистрирует в Bot API **сама** на старте — вручную `setWebhook` вызывать не нужно.
Регистрацию проверьте через Bot API:

```bash theme={null}
curl -s "https://api.telegram.org/botYOUR_BOT_TOKEN/getWebhookInfo"
```

`url` равен `https://auth.your-domain.com/api/channels/telegram/webhook`, `last_error_message`
отсутствует. В логе контейнера есть и строка `Telegram webhook registered: …`, но только на уровне
`Information`: образ в `Production` пишет лог от `Warning`, поэтому там этой строки нет.

Секция валидируется на старте, как и остальные: включённый адаптер без `BotToken`, без
`WebhookSecretToken` или с относительным либо не-HTTPS `WebhookBaseUrl` останавливает контейнер
сообщением, называющим ключ.

<Tip>
  `UpdateMode=Polling` публичного URL не требует — годится, чтобы проверить вход до того, как
  настроен прокси, но не для развёрнутой установки. Опрашивать бота может только один процесс:
  Bot API отдаёт очередь апдейтов единственному клиенту `getUpdates`, поэтому второй инстанс на том
  же токене не получает ничего — см. [Запуск более одного инстанса](#запуск-более-одного-инстанса).
</Tip>

## 8. Проверьте

**Критерий готовности: зелёные шаги 1–4.** Процесс жив, зависимости отвечают, discovery отдаёт
`https`-URL, канал зарегистрирован — установка развёрнута. Шаг 5 проверяет уже вашу интеграцию (клиента
и сценарий), а не сам сервер.

<Steps>
  <Step title="Проверьте liveness">
    `curl -f http://localhost:8080/health/live` — процесс поднят.
  </Step>

  <Step title="Проверьте readiness">
    `curl -f http://localhost:8080/health/ready` — настроенные зависимости (БД, Redis, RabbitMQ)
    отвечают. Именно этот probe стоит завести в оркестратор.
  </Step>

  <Step title="Проверьте discovery">
    `curl https://auth.your-domain.com/.well-known/openid-configuration` возвращает OIDC-метаданные с
    `https`-URL. Если они выходят как `http`, вернитесь к шагу 6.
  </Step>

  <Step title="Проверьте, что канал поднялся">
    Для режима `Webhook`: `getWebhookInfo` бота отдаёт в `url` ваш `/api/channels/telegram/webhook`
    без `last_error_message` — см. [шаг 7](#7-включите-каналы). `url` пуст — подтверждать вход будет
    нечем. В режиме `Polling` такого вызова нет: читайте `/health/channels`, помня, на какие вопросы
    он не отвечает (ниже).
  </Step>

  <Step title="Проведите вход">
    Инициируйте вход из вашего приложения и подтвердите его в доверенном канале на телефоне. У Email —
    и у любого канала при `Veriqa:LoginConfirmation:Mode` = `OnWebPage` — страница входа затем
    спрашивает ещё раз: подтвердите и там.
  </Step>
</Steps>

<Note>
  **На что `/health/channels` отвечает, а на что — нет.** Эндпоинт сообщает состояние каждого
  включённого канала отдельно, и читать нужно именно значение канала: `healthy`, `unhealthy` или
  `timeout`.

  * **HTTP-код всегда `200`.** Отказ канала отдаётся как `Degraded`, но никогда как `Unhealthy`, и
    это сделано намеренно: недоступный мессенджер не должен выбивать работающую реплику из ротации.
    Поэтому `curl -f` успешен даже когда лежат все каналы — алертить нужно по телу ответа, а не по
    коду. По той же причине каналов нет в `/health/ready`.
  * **Отчёт может быть устаревшим.** Это снимок, обновляемый не чаще раза в 15 секунд, и двигает
    его только чтение: запрос, заставший снимок просроченным, всё равно отвечает по старому, а
    обновление запускает в фоне. Первое чтение после падения канала ещё может сказать `healthy`,
    правду скажет следующее, — поэтому, если вы по нему алертите, опрашивайте его периодически.
  * **Он не знает, кому принадлежит очередь апдейтов.** Зонд — аутентифицированный вызов платформы,
    так что неверный или отозванный токен действительно выйдет как `unhealthy`; но токен, который
    валиден и уже занят другим опрашивающим, ответит безупречно. См.
    [Запуск более одного инстанса](#запуск-более-одного-инстанса).
</Note>

## 9. Типичные ошибки первого запуска

Почти всё, что ломается на первом запуске, ломается **на старте** — контейнер не остаётся жить в
полусломанном состоянии, а завершается с сообщением, называющим ключ. Поэтому первое действие при
любом сбое — `docker logs veriqa`.

| Симптом | Причина | Куда идти |
| - | - | - |
| Контейнер завершается: `Set Veriqa:OpenIddict:Server:SigningCertificateBase64 … The parameters are required in a non-Development environment.` — либо то же самое про `EncryptionCertificateBase64` | Вне `Development` нужны **оба** сертификата. Сообщение называет тот, которого не хватило: при обоих незаданных первым срабатывает подпись, при заданной только подписи — шифрование | [Шаг 3](#3-предоставьте-сертификаты-токенов) |
| Контейнер завершается на старте: `CryptographicException: Error occurred during a cryptographic operation`, при заданных `SigningCertificatePath` / `EncryptionCertificatePath` | Смонтированный PFX недоступен на чтение пользователю контейнера (UID `1654`): `openssl` оставляет файл читаемым только владельцу. Сообщение не называет ни путь, ни отказ в доступе, поэтому читается как проблема с паролем или форматом | [Шаг 3](#3-предоставьте-сертификаты-токенов) — `sudo chown 1654` на файлы перед монтированием; `chmod 644` — только там, где нет root, и он делает PFX читаемым любому локальному пользователю |
| Контейнер завершается: `Clients[0] (my-app): AllowedRedirectUris cannot be empty.` (либо `ClientId cannot be empty.`, `AllowedScopes cannot be empty.`, `invalid redirect URI '…'`) | Запись в `Veriqa:OpenIddict:Clients` неполна | [Шаг 4](#4-объявите-свой-клиент) |
| Контейнер завершается: `BotToken is required when the Telegram adapter is enabled.` или `WebhookBaseUrl must use HTTPS (a Telegram Bot API requirement).` | Канал включён, но донастроен не до конца | [Шаг 7](#7-включите-каналы) |
| Контейнер завершается: `Unsupported value 'Veriqa:OpenIddict:Database:Provider' = 'Postgres'. Allowed: 'InMemory', 'PostgreSQL', 'SqlServer'.` | Опечатка в значении провайдера. Резолвер fail-fast: молчаливого отката на `InMemory` нет | [Шаг 2](#2-настройте-его) |
| Контейнер завершается: `Veriqa:Logging:Mode is 'Audit', but the audit store is in memory: set 'Veriqa:Logging:Store:ConnectionString' or 'Veriqa:TransactionEngine:Store:ConnectionString'.` | Режим `Audit` включён, но строки подключения нет ни у стока аудита, ни у хранилища транзакций — вне `Development` записям негде храниться долговечно | [Сток аудита](#сток-аудита) |
| Контейнер завершается: `With 'Veriqa:OpenIddict:Database:Provider' = 'PostgreSQL' you must set 'Veriqa:OpenIddict:Database:ConnectionString'.` | Реляционный провайдер задан, строка подключения — нет. У стора транзакций правило другое: там **отсутствие** строки подключения штатно выбирает хранилище в памяти | [Шаг 2](#2-настройте-его) |
| Контейнер жив, но `/.well-known/openid-configuration` отвечает `invalid_request` («This server only accepts HTTPS requests») | Прокси не перечислен в доверенных, `X-Forwarded-Proto` игнорируется | [Шаг 6](#6-поставьте-за-reverse-proxy) |
| Контейнер `healthy` и `/health/ready` отвечает 200, но `/connect/authorize` отвечает `503` с `"title": "channel_display_failed"` | Токен канала неверный или отозван, а канал работает в режиме `Polling`: ключ задан, поэтому канал проходит проверку на старте, а платформа отвергает токен только при подготовке страницы входа. В режиме `Webhook` тот же токен срывает регистрацию вебхука уже на старте, и контейнер завершается | `/health/channels` — канал помечен `unhealthy` (в readiness каналы намеренно не входят); причина — в `docker logs veriqa` |
| Контейнер завершается на старте с `bad webhook: …` (например, `bad webhook: Failed to resolve host`), токен верный | Telegram не достучался до `WebhookBaseUrl`: имя хоста ещё не распространилось в DNS, сертификат не принят или адрес недоступен из интернета | Дождитесь, пока имя резолвится через внешний резолвер (`dig @8.8.8.8 auth.your-domain.com`), и запустите снова — либо начните с `UpdateMode=Polling`, [шаг 7](#7-включите-каналы) |
| Страница входа открывается, но в мессенджер ничего не приходит (или кнопка не отвечает) | Путь `/api/channels/telegram/webhook` не выставлен прокси наружу, либо `WebhookBaseUrl` указывает не на публичный адрес | [Шаг 7](#7-включите-каналы) |
| То же самое в режиме `Polling` — и при этом `/health/channels` по-прежнему показывает канал `healthy` | Этого бота уже опрашивает другой процесс, чаще всего оставленная работать staging-установка на боевом токене. Bot API отдаёт очередь апдейтов одному клиенту `getUpdates`, и проигравший не получает ничего; токен у него при этом остаётся валидным, поэтому зонд здоровья проблемы не видит | `docker logs veriqa` — повторяющаяся ошибка цикла опроса с текстом `Conflict: terminated by other getUpdates request` (номера `409` в самой записи нет). Один опрашивающий на бота: [Запуск более одного инстанса](#запуск-более-одного-инстанса) |

## Запуск более одного инстанса

Одному контейнеру не нужно ничего дополнительно. Масштабирование требует двух общих кусков состояния:

| Зона | Настройка | Зачем |
| - | - | - |
| SignalR backplane | `Veriqa:SignalR:RedisConnectionString` | Живой статус входа должен доходить до браузера независимо от того, к какому инстансу он подключён |
| Ключи DataProtection | `Veriqa:DataProtection:RedisConnectionString` или `:KeysDirectory` | Без общих ключей инстансы не читают cookie и токены друг друга |

Оставьте оба незаданными — и каждый инстанс держит свои ключи в памяти: нормально для одного
контейнера, сломано для нескольких. Общий том для `KeysDirectory` работает как альтернатива Redis.

Само собой не масштабируется ещё одно: **доставка апдейтов в режиме `Polling`**. Bot API отдаёт
очередь апдейтов единственному клиенту `getUpdates`, поэтому второй инстанс на том же токене бота
не получает вообще ничего — и при этом считает себя `healthy`, ведь токен валиден и платформа ему
отвечает. Выдаёт его только лог: цикл опроса повторяет отказ `409` Bot API, записанный как
`Conflict: terminated by other getUpdates request` — самого номера в строке нет. Масштабируйтесь
на режиме `Webhook`, где каждый инстанс кормит прокси, либо дайте каждой установке своего бота —
последнее касается staging-установки рядом с боевой ровно так же, как второй реплики.

## Дальше

<CardGroup cols={2}>
  <Card title="Настройка каналов" icon="comments" href="/docs/ru/guides/channels">
    MAX, Telegram, WhatsApp, другие мессенджеры и Email — от начала до конца.
  </Card>

  <Card title="Справочник конфигурации" icon="list" href="/docs/ru/reference/configuration">
    Каждая секция, ключ и дефолт в одном месте.
  </Card>

  <Card title="Продакшн-харденинг" icon="shield-check" href="/docs/ru/guides/hardening">
    Что проверить перед выкаткой.
  </Card>

  <Card title="Хранилища" icon="database" href="/docs/ru/guides/storage">
    Хранилище транзакций, ретенция и миграции.
  </Card>

  <Card title="Установка службой ОС" icon="server" href="/docs/ru/quickstart/os-service">
    Тот же сервер без Docker — systemd или служба Windows, из архива.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.