# 🐳 Docker Guide — OmniRoute (Русский) 🌐 **Languages:** 🇺🇸 [English](../../../../guides/DOCKER_GUIDE.md) · 🇪🇹 [am](../../../am/docs/guides/DOCKER_GUIDE.md) · 🇸🇦 [ar](../../../ar/docs/guides/DOCKER_GUIDE.md) · 🇦🇿 [az](../../../az/docs/guides/DOCKER_GUIDE.md) · 🇧🇬 [bg](../../../bg/docs/guides/DOCKER_GUIDE.md) · 🇧🇩 [bn](../../../bn/docs/guides/DOCKER_GUIDE.md) · 🇧🇦 [bs](../../../bs/docs/guides/DOCKER_GUIDE.md) · 🇨🇿 [cs](../../../cs/docs/guides/DOCKER_GUIDE.md) · 🇩🇰 [da](../../../da/docs/guides/DOCKER_GUIDE.md) · 🇩🇪 [de](../../../de/docs/guides/DOCKER_GUIDE.md) · 🇬🇷 [el](../../../el/docs/guides/DOCKER_GUIDE.md) · 🇪🇸 [es](../../../es/docs/guides/DOCKER_GUIDE.md) · 🇪🇪 [et](../../../et/docs/guides/DOCKER_GUIDE.md) · 🇮🇷 [fa](../../../fa/docs/guides/DOCKER_GUIDE.md) · 🇫🇮 [fi](../../../fi/docs/guides/DOCKER_GUIDE.md) · 🇫🇷 [fr](../../../fr/docs/guides/DOCKER_GUIDE.md) · 🇮🇪 [ga](../../../ga/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [gu](../../../gu/docs/guides/DOCKER_GUIDE.md) · 🇳🇬 [ha](../../../ha/docs/guides/DOCKER_GUIDE.md) · 🇮🇱 [he](../../../he/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [hi](../../../hi/docs/guides/DOCKER_GUIDE.md) · 🇭🇷 [hr](../../../hr/docs/guides/DOCKER_GUIDE.md) · 🇭🇺 [hu](../../../hu/docs/guides/DOCKER_GUIDE.md) · 🇦🇲 [hy](../../../hy/docs/guides/DOCKER_GUIDE.md) · 🇮🇩 [id](../../../id/docs/guides/DOCKER_GUIDE.md) · 🇳🇬 [ig](../../../ig/docs/guides/DOCKER_GUIDE.md) · 🇮🇹 [it](../../../it/docs/guides/DOCKER_GUIDE.md) · 🇯🇵 [ja](../../../ja/docs/guides/DOCKER_GUIDE.md) · 🇬🇪 [ka](../../../ka/docs/guides/DOCKER_GUIDE.md) · 🇰🇭 [km](../../../km/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [kn](../../../kn/docs/guides/DOCKER_GUIDE.md) · 🇰🇷 [ko](../../../ko/docs/guides/DOCKER_GUIDE.md) · 🇱🇹 [lt](../../../lt/docs/guides/DOCKER_GUIDE.md) · 🇱🇻 [lv](../../../lv/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [ml](../../../ml/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [mr](../../../mr/docs/guides/DOCKER_GUIDE.md) · 🇲🇾 [ms](../../../ms/docs/guides/DOCKER_GUIDE.md) · 🇲🇹 [mt](../../../mt/docs/guides/DOCKER_GUIDE.md) · 🇲🇲 [my](../../../my/docs/guides/DOCKER_GUIDE.md) · 🇳🇵 [ne](../../../ne/docs/guides/DOCKER_GUIDE.md) · 🇳🇱 [nl](../../../nl/docs/guides/DOCKER_GUIDE.md) · 🇳🇴 [no](../../../no/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [or](../../../or/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [pa](../../../pa/docs/guides/DOCKER_GUIDE.md) · 🇵🇭 [phi](../../../phi/docs/guides/DOCKER_GUIDE.md) · 🇵🇱 [pl](../../../pl/docs/guides/DOCKER_GUIDE.md) · 🇵🇹 [pt](../../../pt/docs/guides/DOCKER_GUIDE.md) · 🇧🇷 [pt-BR](../../../pt-BR/docs/guides/DOCKER_GUIDE.md) · 🇷🇴 [ro](../../../ro/docs/guides/DOCKER_GUIDE.md) · 🇱🇰 [si](../../../si/docs/guides/DOCKER_GUIDE.md) · 🇸🇰 [sk](../../../sk/docs/guides/DOCKER_GUIDE.md) · 🇸🇮 [sl](../../../sl/docs/guides/DOCKER_GUIDE.md) · 🇷🇸 [sr](../../../sr/docs/guides/DOCKER_GUIDE.md) · 🇸🇪 [sv](../../../sv/docs/guides/DOCKER_GUIDE.md) · 🇰🇪 [sw](../../../sw/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [ta](../../../ta/docs/guides/DOCKER_GUIDE.md) · 🇮🇳 [te](../../../te/docs/guides/DOCKER_GUIDE.md) · 🇹🇭 [th](../../../th/docs/guides/DOCKER_GUIDE.md) · 🇹🇷 [tr](../../../tr/docs/guides/DOCKER_GUIDE.md) · 🇺🇦 [uk-UA](../../../uk-UA/docs/guides/DOCKER_GUIDE.md) · 🇵🇰 [ur](../../../ur/docs/guides/DOCKER_GUIDE.md) · 🇺🇿 [uz](../../../uz/docs/guides/DOCKER_GUIDE.md) · 🇻🇳 [vi](../../../vi/docs/guides/DOCKER_GUIDE.md) · 🇳🇬 [yo](../../../yo/docs/guides/DOCKER_GUIDE.md) · 🇨🇳 [zh-CN](../../../zh-CN/docs/guides/DOCKER_GUIDE.md) · 🇹🇼 [zh-TW](../../../zh-TW/docs/guides/DOCKER_GUIDE.md) --- > Полное справочное руководство по развертыванию Docker. Для быстрого старта см. [раздел Docker в README](../README.md#-docker). ## Содержание - [Быстрый запуск](#quick-run) - [С файлом окружения](#with-environment-file) - [Docker Compose](#docker-compose) - [Доступные профили](#available-profiles) - [Настройка инструментов CLI хоста при запуске OmniRoute в Docker](#configuring-host-cli-tools-when-omniroute-runs-in-docker) - [Вспомогательный контейнер Redis](#redis-sidecar) - [Compose для продакшена](#production-compose) - [Стадии Dockerfile](#dockerfile-stages) - [Критически важные переменные окружения](#critical-environment-variables) - [Docker Compose с Caddy (HTTPS)](#docker-compose-with-caddy-https-auto-tls) - [Быстрый туннель Cloudflare](#cloudflare-quick-tunnel) - [Теги образов](#image-tags) - [Обновления панели управления в Docker](#dashboard-updates-in-docker) - [Доступность: SQLite по умолчанию поддерживает только одну реплику](#availability-default-sqlite-is-single-replica) - [Региональные ошибки Gemini внутри Docker](#gemini-regional-errors-inside-docker) - [Важные примечания](#important-notes) --- ## Быстрый запуск > **Самостоятельный хостинг одной командой?** См. > [руководство по самостоятельному хостингу](../getting-started/SELF_HOST_GUIDE.md) — > `docker compose -f docker-compose.selfhost.yml up -d` (опубликованный образ + > Redis, доступ только через loopback-интерфейс, без выбора профиля). Приведенный ниже быстрый запуск — > это вариант с одним контейнером для пользователей, у которых Redis уже запущен в другом месте. ```bash docker run -d \ --name omniroute \ --restart unless-stopped \ --stop-timeout 40 \ -p 20128:20128 \ -v omniroute-data:/app/data \ diegosouzapw/omniroute:latest ``` ## С файлом окружения ```bash # Сначала скопируйте и отредактируйте .env cp .env.example .env docker run -d \ --name omniroute \ --restart unless-stopped \ --stop-timeout 40 \ --env-file .env \ -p 20128:20128 \ -v omniroute-data:/app/data \ diegosouzapw/omniroute:latest ``` ## Docker Compose ```bash # Базовый профиль (без инструментов CLI) docker compose --profile base up -d # Профиль CLI (встроенные Claude Code, Codex и OpenClaw) docker compose --profile cli up -d # Профиль хоста (прежде всего для Linux; монтирует двоичные файлы CLI хоста только для чтения) docker compose --profile host up -d # Веб-профиль (Chromium/Playwright для провайдеров веб-сеансов) docker compose --profile web up -d # Совместное использование CLI и вспомогательного контейнера CLIProxyAPI docker compose --profile cli --profile cliproxyapi up -d ``` ## Доступные профили OmniRoute предоставляет профили Compose для основных вариантов развертывания. Выберите профиль, соответствующий вашей среде. | Профиль | Сервис | Когда использовать | Команда | | --------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | `base` (по умолчанию) | `omniroute-base` | Безголовый сервер / минимальная среда выполнения, без встроенных CLI провайдеров | `docker compose --profile base up -d` | | `cli` | `omniroute-cli` | Агентные рабочие процессы, вызывающие `omniroute providers/setup/doctor` и встроенные CLI (Codex, Claude Code, Droid, OpenClaw) | `docker compose --profile cli up -d` | | `host` | `omniroute-host` | Хосты Linux, которым необходим аналогичный `network_mode` доступ к CLI хоста посредством монтирования `~/.local/bin`, `~/.codex`, `~/.claude` и т. д. только для чтения | `docker compose --profile host up -d` | | `cliproxyapi` | `cliproxyapi` | Запуск вспомогательного контейнера [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) на порту `8317` для проксирования вышестоящих CLI | `docker compose --profile cliproxyapi up -d` | | `web` | `omniroute-web` | Провайдеры веб-сеансов, которым требуется браузер: `gemini-web`, `claude-web`, `claude-turnstile` (собирает `runner-web`, Chromium включен) | `docker compose --profile web up -d` | > Можно объединять несколько профилей: `docker compose --profile cli --profile cliproxyapi up -d`. ## Настройка CLI-инструментов хоста, когда OmniRoute работает в Docker `omniroute setup-codex`, `setup-claude`, `config set ` и кнопка **Сохранить конфигурацию** на панели управления записывают файлы наподобие `~/.codex/*.config.toml`. Эти пути имеют смысл только на машине, где фактически работает CLI. Если запустить эти команды внутри контейнера, запись будет выполнена в собственный домашний каталог контейнера (`/home/node` — образ работает от имени `USER node`), откуда ни один CLI на хосте никогда не прочитает данные и где они будут удалены при пересоздании контейнера. OmniRoute обнаруживает такую ситуацию и вместо сообщения об успехе, которым невозможно воспользоваться, отклоняет запись и выводит инструкции: CLI завершается с кодом `2`, а API отвечает `422` с `containerEphemeralTarget: true`. ### Рекомендуемый вариант: запускать CLI на хосте, а OmniRoute — в Docker Контейнер предоставляет API, а CLI настраивает ваши инструменты на хосте. ```bash docker compose --profile base up -d npm install -g omniroute omniroute connect http://localhost:20128 # направить CLI на контейнер omniroute setup-codex # записывает данные в настоящий каталог ~/.codex на вашем хосте ``` Это правильный выбор, когда Codex, Claude Code, Cursor или аналогичные инструменты работают на вашем ноутбуке — а это обычная конфигурация. ### Альтернатива: bind-монтирование каталогов конфигурации хоста (профиль `host`) Если вы хотите, чтобы сам контейнер записывал конфигурацию на хост, подключите каталоги через монтирование и укажите корень монтирования в `CLI_CONFIG_HOME`. Профиль `host` уже настроен таким образом: ```yaml environment: - CLI_CONFIG_HOME=/host-home - CLI_ALLOW_CONFIG_WRITES=true volumes: - ~/.codex:/host-home/.codex:rw - ~/.claude:/host-home/.claude:rw ``` Именно bind-монтирование делает путь доверенным: OmniRoute читает `/proc/self/mountinfo` и разрешает запись в смонтированные пути (а также в каталоги, дочерние элементы которых являются точками монтирования, что в точности соответствует приведённой выше структуре `/host-home`), при этом по-прежнему отклоняя запись в несмонтированные пути. ### Обходной вариант: настройка собственных CLI контейнера (используйте с осторожностью) Если CLI действительно находятся внутри контейнера (профиль `cli`), такая запись является намеренной. Передайте `--allow-container-write` любой команде `setup-*` или задайте `OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE=true` для сервера. Запись будет выполнена с предупреждением о том, что она не сохранится после удаления контейнера. > **Предупреждение о безопасности — профиль `cli` + монтирование `docker.sock`.** > Профиль `cli` bind-монтирует `/var/run/docker.sock`, чтобы работающий внутри контейнера > модуль автоматического обновления мог пересоздавать стек через демон хоста > (`src/lib/system/autoUpdate.ts` проверяет наличие этого сокета и не использует > путь Docker, если сокет отсутствует). Этот сокет является **границей доверия с root-доступом > к хосту**: всё, что может получить к нему доступ, управляет демоном Docker на хосте от имени > root — и может создавать, проверять, останавливать и удалять любые контейнеры на хосте. > Следствия: > > 1. **Никогда не открывайте порт профиля `cli` для доступа из сети.** Публикуйте > его на `127.0.0.1` (`ports: "127.0.0.1:${DASHBOARD_PORT:-20128}:..."`) > — профиль `cli`, доступный из локальной сети, превращает любую возможность RCE на уровне панели управления > в полную компрометацию хоста. > 2. **Не подключайте дополнительные каталоги хоста к профилю `cli`.** > Сокет Docker в сочетании с любым дополнительным монтированием предоставляет контейнеру полный > доступ на чтение и запись к вашей файловой системе и конфигурации хоста. Если инструменту требуется > доступ к проекту, запускайте его локально с помощью исполняемого файла CLI — не монтируйте проект > в контейнер `cli`. > > Если вам не требуется автоматическое обновление внутри контейнера, не включайте профиль `cli` > (`COMPOSE_PROFILES=core,redis` или более короткий вариант). Остальные профили не > монтируют сокет Docker. > > Связанную модель угроз для MITM см. в `docs/security/MITM-TPROXY-DECRYPT.md` (git; не включается в собранный каталог `/docs`), > а цепочку происхождения бинарных файлов `codex`/`claude-code`/`droid`/`openclaw` — > в `docs/security/SUPPLY_CHAIN.md`. ## Сайдкар Redis OmniRoute использует Redis для обеспечения работы распределённого ограничителя частоты запросов и общего кэша. Сервис `redis` **всегда определён** в `docker-compose.yml` (он не ограничен профилем) и запускается вместе с любым другим профилем. | Параметр | Значение | | --------------------------------------- | -------------------------------------------- | | Образ | `redis:7-alpine` | | Имя контейнера | `omniroute-redis` | | Внутренний порт | `6379` | | Порт хоста (переопределяемый) | `REDIS_PORT` (по умолчанию `6379`) | | Адрес привязки хоста (переопределяемый) | `REDIS_BIND_HOST` (по умолчанию `127.0.0.1`) | | Том | `omniroute-redis-data` → `/data` | | Проверка работоспособности | `redis-cli ping` (интервал 10 с) | Связанные переменные окружения: - `REDIS_URL` — строка подключения, передаваемая приложению (по умолчанию `redis://redis:6379`). - `REDIS_PORT` — порт хоста, сопоставленный с контейнером Redis. - `REDIS_BIND_HOST` — сетевой интерфейс хоста, на котором публикуется порт. По умолчанию `127.0.0.1`. > **Почему по умолчанию используется loopback-интерфейс:** сайдкар запускается без `requirepass`, а контейнеры > приложения обращаются к нему через сеть Compose (`redis:6379`) — опубликованный порт > нужен только для инструментов на стороне хоста (`redis-cli`, локальный `npm run dev`). Публикация на > `0.0.0.0` сделала бы Redis без аутентификации доступным каждому хосту в вашей локальной сети. Если вы задаёте > `REDIS_BIND_HOST=0.0.0.0`, также добавьте `--requirepass` в `command:` сервиса. **Отключать Redis** не рекомендуется (ограничитель частоты запросов переключится на менее эффективный резервный вариант в памяти). Если это необходимо, удалите или закомментируйте блок сервиса `redis:` в `docker-compose.yml` либо уменьшите количество его экземпляров до нуля: ```bash docker compose up -d --scale redis=0 ``` ## Production Compose Для изолированного production-снимка, работающего параллельно со средой разработки, используйте `docker-compose.prod.yml`. | Параметр | Значение | | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | Файл | `docker-compose.prod.yml` | | Порт панели управления по умолчанию | `PROD_DASHBOARD_PORT=20130` (сопоставляется с внутренним `${DASHBOARD_PORT:-20128}`) | | Порт API по умолчанию | `PROD_API_PORT=20131` | | Образ | `omniroute:prod` (собирается из целевого этапа `runner-cli`) | | Контейнер Redis | `omniroute-redis-prod` (`redis:8.6.2`, выделенный том `redis-prod-data`) | | Том данных | `omniroute-prod-data` (именованный, сохраняется при повторных сборках) | | Проверки работоспособности | `node healthcheck.mjs` + `redis-cli ping`, при этом `depends_on` ожидает успешной проверки Redis | Использование: ```bash # Сборка и запуск production-стека docker compose -f docker-compose.prod.yml up -d --build # Потоковый просмотр журналов docker compose -f docker-compose.prod.yml logs -f # Остановка и удаление стека (с сохранением томов) docker compose -f docker-compose.prod.yml down ``` Production-стек работает параллельно со средой разработки Compose (используются другие имена контейнеров, порты и тома), поэтому можно продолжать локальную разработку, не останавливая production-среду. ## Этапы Dockerfile Репозиторий содержит многоэтапный Dockerfile (`Dockerfile`). Доступны четыре этапа; выберите подходящий `target` для вашего сценария использования. | Этап | Базовый образ | Назначение | | ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `builder` | `node:26-trixie-slim` | Устанавливает зависимости (`npm ci --legacy-peer-deps`) и запускает `npm run build` (по умолчанию используется Turbopack — см. раздел «Ресурсы во время сборки» ниже) | | `runner-base` | `node:26-trixie-slim` | Среда выполнения для продакшена с автономным выводом Next.js. **CLI провайдеров не включены.** | | `runner-cli` | `runner-base` | Добавляет `git`, `docker.io`, `docker-compose` и глобальные CLI: `@openai/codex`, `@anthropic-ai/claude-code`, `droid`, `openclaw`. **Выберите этот этап для агентных рабочих процессов.** | | `runner-web` | `runner-base` | Добавляет Playwright и браузер Chromium (`--with-deps`) для провайдеров веб-сессий: `gemini-web`, `claude-web`, `claude-turnstile`. **Выберите этот этап при использовании этих провайдеров** — обычный образ без него завершается ошибкой во время запроса (см. примечание о `-web` в разделе «Каналы выпуска»). | Сборка конкретного этапа вручную: ```bash docker build --target runner-base -t omniroute:base . docker build --target runner-cli -t omniroute:cli . docker build --target runner-web -t omniroute:web . ``` ### Ресурсы во время сборки Три аргумента сборки определяют потребление ресурсов этапом `builder`. Они действуют только во время сборки — `OMNIROUTE_MEMORY_MB` (см. ниже) является отдельным параметром среды выполнения. | Аргумент сборки | По умолчанию | Эффект | | --------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | | `OMNIROUTE_USE_TURBOPACK` | `0` | `0` выполняет сборку с webpack: меньше пиковое потребление памяти, но медленнее. `1` включает Turbopack. | | `OMNIROUTE_BUILD_MEMORY_MB` | `6144` | Верхний предел кучи V8 (`--max-old-space-size`) для запускаемого процесса `next build`. | | `OMNIROUTE_BUILD_WORKERS` | `2` | Передаётся в `CIRCLE_NODE_TOTAL`; Next вычисляет `workers = N - 1` для сбора данных страниц. | `OMNIROUTE_BUILD_WORKERS` следует увеличивать на мощной машине сборки и проверять в первую очередь, если сборка в среде с ограниченными ресурсами завершается **после** `✓ Compiled successfully`. Каждый воркер обработки данных страниц является отдельным процессом, как и сам родительский процесс `next build`; воспроизведение на реальном VPS (issue #7518) показало, что пиковый RSS каждого процесса составляет ~4.5 GB независимо от параметра кучи `NODE_OPTIONS` (Turbopack выполняет компиляцию в нативной памяти/Rust за пределами кучи V8). Значение по умолчанию `2` (→ 1 воркер, всего 2 процесса) рассчитано на размещённые в GitHub среды выполнения с 16 GB / 4 vCPU, которые используются конвейером публикации. При значении `8` (→ 7 воркеров) в такой среде закончилась память, и buildkit завершил этап с ошибкой `ResourceExhausted: ... cannot allocate memory`; значение `3` (→ 2 воркера) также не уложилось в ограничения после того, как RSS каждого процесса был измерен напрямую, а не вычислен косвенно. `tests/unit/docker-build-memory-budget.test.ts` выполняет расчёты на основе измеренного значения и завершается ошибкой, если любой из этих параметров превышает возможности среды выполнения. Turbopack выполняет компиляцию в нативной памяти Rust, расположенной **за пределами** кучи V8, поэтому `OMNIROUTE_BUILD_MEMORY_MB` её не ограничивает. На хосте с ограничением памяти процесс сборки в таком случае принудительно завершается OOM-механизмом через SIGKILL вообще без текста ошибки — он просто останавливается посреди `Creating an optimized production build`, что больше похоже на зависание, чем на нехватку памяти. Именно поэтому в `Dockerfile` по умолчанию используется webpack (`OMNIROUTE_USE_TURBOPACK=0`), в отличие от `npm run dev` / `npm run build`, где Turbopack используется по умолчанию на уровне кода: обычная команда `docker build .` без аргументов сборки (именно её запускают Railway и другие платформы развёртывания в один клик) не должна аварийно завершаться без сообщения в среде сборки с ограниченной памятью. Для опубликованных образов значение `OMNIROUTE_USE_TURBOPACK=0` уже явно передаётся в `docker-publish.yml`. На машине сборки с большим объёмом оперативной памяти включите Turbopack для ускорения сборки: ```bash docker build --target runner-base \ --build-arg OMNIROUTE_USE_TURBOPACK=1 \ -t omniroute:base . ``` `webpackBuildWorker` включён, поэтому `next build` запускает родительский процесс **и** процесс-воркер, каждый из которых отдельно учитывает `OMNIROUTE_BUILD_MEMORY_MB`. Устанавливайте лимит контейнера примерно вдвое выше этого значения, а не равным ему. Результаты измерений для этого дерева (`--target runner-base`, `OMNIROUTE_BUILD_MEMORY_MB=6144`): | Сборщик | Лимит контейнера | Результат | | --------- | ---------------- | ----------------------------------------------------- | | Turbopack | 8 GiB / 16 GiB | в обоих случаях завершён OOM-механизмом без сообщения | | webpack | 8 GiB | процесс сборки завершён через SIGKILL | | webpack | 12 GiB | успешно, пиковое потребление — 11.1 GiB | ### Значения по умолчанию во время выполнения Значения по умолчанию, экспортируемые `runner-base`: `PORT=20128`, `HOSTNAME=0.0.0.0`, `OMNIROUTE_MEMORY_MB=1024`, `NODE_OPTIONS=--max-old-space-size=1024`, `DATA_DIR=/app/data`, `OMNIROUTE_MIGRATIONS_DIR=/app/migrations`. Поведение памяти в Docker: - Образ задаёт `OMNIROUTE_MEMORY_MB=1024` и на его основе формирует `NODE_OPTIONS=--max-old-space-size=1024`. - Фактический серверный процесс запускается автономным средством запуска, которое считывает `OMNIROUTE_MEMORY_MB` и добавляет `--max-old-space-size=`. - Node использует последнее из повторяющихся значений `--max-old-space-size`, поэтому параметр `OMNIROUTE_MEMORY_MB` управляет фактическим ограничением кучи в Docker. - Поскольку образ всегда задаёт этот параметр, собственное резервное значение средства запуска, рассчитываемое на основе объёма ОЗУ, в Docker никогда не применяется. Явно увеличьте его в соответствии с рабочей нагрузкой (см. таблицу ниже). `2048` по-прежнему недостаточно для `/v1/responses` агентов программирования. ### Оперативная память для агентов программирования во время выполнения Значение Docker по умолчанию в 1 ГиБ — это минимум для панели управления и лёгкого чата, а не объём для эксплуатации в производственной среде. При обработке длинных тел `POST /v1/responses` (сотни сообщений, десятки инструментов) во время сжатия в памяти сохраняются несколько графов. Два перекрывающихся запроса объёмом примерно 3 МиБ / 750 тыс. токенов приводили к аварийному завершению V8 при **12 ГиБ** old-space (`FATAL ERROR: Reached heap limit`), а также к OOM в cgroup размером 16 ГиБ. См. [#7849](https://github.com/diegosouzapw/OmniRoute/issues/7849). Задавайте размер **cgroup `--memory` больше размера кучи** — нативные буферы, SQLite и промежуточные данные сжатия размещаются вне V8. | Рабочая нагрузка | `OMNIROUTE_MEMORY_MB` | Контейнер / cgroup | Примечания | | ------------------------------------------------ | ------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Панель управления, один лёгкий чат | `1024` (по умолчанию в образе) | ≥2 ГиБ | | | Один агент программирования (Claude/Codex/Grok) | `8192` | ≥10 ГиБ | Типичный одиночный сеанс `/v1/responses` | | Два параллельных длинных запроса `/v1/responses` | `10240`–`12288` | ≥12–16 ГиБ | Зафиксировано аварийное завершение V8 при куче размером около 12 ГиБ | | Три и более параллельных длинных контекста | не используйте в одном процессе | выполняйте последовательно / больше ОЗУ | По умолчанию допускается 1 выполняющийся ресурсоёмкий запрос; увеличение этого значения без дополнительной ОЗУ снова приведёт к аварийному завершению | При запуске `omniroute serve` без контейнера объём кучи рассчитывается примерно как 35% ОЗУ (с ограничением диапазоном `[512, 4096]`), если `OMNIROUTE_MEMORY_MB` **не задана**. Docker всегда задаёт значение `1024`, поэтому в официальном образе такой расчёт никогда не выполняется. ```bash docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \ -e OMNIROUTE_MEMORY_MB=8192 --memory=10g \ -p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest ``` ## Критически важные переменные окружения Помимо значений по умолчанию, описанных в [ENVIRONMENT.md](../reference/ENVIRONMENT.md), при запуске в Docker наиболее важны следующие переменные: | Переменная | Назначение | Значение по умолчанию | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `OMNIROUTE_WS_BRIDGE_SECRET` | Общий секрет для моста WebSocket. **Обязателен в рабочей среде** — задайте стойкую случайную строку. | не задано (необходимо указать) | | `REDIS_URL` | Строка подключения к бэкенду ограничения частоты запросов / кэша | `redis://redis:6379` | | `REDIS_PORT` | Порт хоста для входящего в комплект контейнера Redis | `6379` | | `REDIS_BIND_HOST` | Интерфейс хоста, на котором публикуется порт входящего в комплект Redis (кольцевой интерфейс, если вы не добавили AUTH) | `127.0.0.1` | | `AUTO_UPDATE_HOST_REPO_DIR` | Путь на хосте, подключаемый в профиль `cli` по адресу `/workspace/omniroute` для рабочих процессов самообновления | `.` (текущий каталог) | | `OMNIROUTE_MEMORY_MB` | Максимальный размер кучи Node во время выполнения для автономного сервера Docker; переопределяет указанное выше значение по умолчанию для образа. Для агентов программирования: `8192`+ (см. [ОЗУ среды выполнения](#runtime-ram-for-coding-agents)). | `1024` | | `DASHBOARD_PORT` / `API_PORT` | Переопределяют опубликованные порты панели управления (20128) и API (20129) | `20128` / `20129` | | `APP_BIND_HOST` | Интерфейс хоста, на котором docker-compose публикует порты панели управления/API/live-WS. При `REQUIRE_API_KEY=false` (значение по умолчанию) `0.0.0.0` открывает анонимный прокси `/v1` для локальной сети — расширяйте доступ только при `REQUIRE_API_KEY=true` или наличии обратного прокси перед приложением. | `127.0.0.1` | | `CLIPROXY_BIND_HOST` | Интерфейс хоста, на котором docker-compose публикует сайдкар `cliproxyapi`; его том данных содержит учетные данные поставщика. | `127.0.0.1` | | `OMNIROUTE_PLUGINS_DIR` | Каталог, который сканер плагинов среды выполнения читает и использует для установки. Задайте его, если плагины подключаются через bind mount: путь по умолчанию зависит от `HOME`, которую образ может не экспортировать. | `~/.omniroute/plugins` | | `OMNIROUTE_BASE_PATH` | Подпуть URL, когда приложение публикуется за обратным прокси (например, `/omniroute`) | _(пусто = корневой путь)_ | | `NEXT_PUBLIC_BASE_URL` | Публичный источник для браузера, включая подпуть (например, `https://host/omniroute`) | не задано | | `PROD_DASHBOARD_PORT` | Порт панели управления на стороне хоста для `docker-compose.prod.yml` | `20130` | | `CLIPROXYAPI_PORT` | Порт на стороне хоста для сайдкара `cliproxyapi` | `8317` | ## Обратный прокси на подпути (Traefik / nginx) `basePath` Next.js компилируется в автономный пакет. OmniRoute записывает встроенное значение в сигнальный файл в корне приложения (записывается во время `npm run build`; читается `scripts/docker/ensure-docker-base-path.mjs`) и сравнивает его с `OMNIROUTE_BASE_PATH` при запуске контейнера. Если значения различаются, а образ был собран для корня домена, точка входа перед запуском `node dev/run-standalone.mjs` перезаписывает автономные манифесты, встроенные литералы `basePath`/`assetPrefix` (Next 16 формирует URL ресурсов SSR только на основе `assetPrefix` — патчер также добавляет в него подпуть), встроенные URL ресурсов `/_next/static` (манифесты клиентских ссылок, импорты медиафайлов, предварительно отрендеренные страницы ошибок) и клиентскую заглушку `process.env`. ### Сборка с помощью Compose (рекомендуется) Задайте обе переменные в `.env`, затем пересоберите образ, чтобы настройки образа и среды выполнения совпадали: ```bash # .env OMNIROUTE_BASE_PATH=/omniroute NEXT_PUBLIC_BASE_URL=https://myhostname.example.com/omniroute ``` ```bash docker compose --profile base up -d --build ``` `docker-compose.yml` передаёт `OMNIROUTE_BASE_PATH` как аргумент сборки Docker и как переменную среды выполнения. ### Предварительно собранный корневой образ + подпуть во время выполнения Опубликованные образы `diegosouzapw/omniroute:*` собраны для корня домена. Тем не менее можно задать `OMNIROUTE_BASE_PATH` во время выполнения; при запуске контейнер однократно исправит пакет. Укажите также соответствующий публичный источник: ```yaml services: omniroute: image: diegosouzapw/omniroute:latest environment: OMNIROUTE_BASE_PATH: /omniroute NEXT_PUBLIC_BASE_URL: https://myhostname.example.com/omniroute ``` Настройте обратный прокси так, чтобы он передавал **полный** внешний путь (не удаляйте префикс). Traefik должен направлять `PathPrefix(`/omniroute`)` в контейнер без `StripPrefix`, чтобы Next.js получал `/omniroute/...` и отдавал ресурсы из `/omniroute/_next/...`. Проверка работоспособности Docker обращается к облегчённой конечной точке жизненного цикла `/healthz` с префиксом активного значения `OMNIROUTE_BASE_PATH`. `/api/monitoring/health` остаётся доступной для диагностики пользователями и панелями мониторинга; чтобы снова направить контейнерную проверку HEALTHCHECK на неё (например, для углублённой проверки работоспособности), задайте `OMNIROUTE_HEALTHCHECK_PATH=/api/monitoring/health`. Этот путь выполняет **глубокую** проверку (БД + сводка мониторинга) — она подходит для нечастого Docker `HEALTHCHECK`, если вы решите снова её включить, но **не** для интервалов Kubernetes `livenessProbe`. Для оркестраторов (Kubernetes, Nomad и т. д.): | Проверка | Предпочтительно | Избегать | | ------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | Жизнеспособность | HTTP `GET /livez` или TCP на основном порту (`PORT`, по умолчанию `20128`) | Использования `/api/monitoring/health` для проверки жизнеспособности | | Готовность | HTTP `GET /healthz` | Малых тайм-аутов, при которых занятый цикл событий считается отказавшим | | Глубокая / внешняя | `/api/monitoring/health` | — | `/healthz` сообщает о состоянии жизненного цикла процесса (`ok` / `starting` / `stopping`). `/livez` проверяет только активность процесса (возвращает 200 всякий раз, когда обработчик может выполниться; готовности он не ожидает). Обе конечные точки по-прежнему выполняются в том же цикле событий Node, что и обработка запросов, поэтому нагружающие CPU операции с каталогом или сжатием могут задерживать ответы — занят ≠ мёртв. Если HTTP-проверки завершаются по тайм-ауту, для проверки жизнеспособности предпочтительнее использовать TCP. Полные рекомендации по проверкам: [Руководство по мониторингу — рекомендации по проверкам Kubernetes](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations). ## Docker Compose с Caddy (автоматический TLS для HTTPS) OmniRoute можно безопасно опубликовать с помощью автоматического выпуска SSL-сертификатов в Caddy. Убедитесь, что DNS-запись A вашего домена указывает на IP-адрес вашего сервера. ```yaml services: omniroute: image: diegosouzapw/omniroute:latest container_name: omniroute restart: unless-stopped volumes: - omniroute-data:/app/data environment: - PORT=20128 # Доступный браузеру адрес для обратных вызовов OAuth, ссылок панели управления и создаваемых публичных URL. - NEXT_PUBLIC_BASE_URL=https://your-domain.com # Внутренний URL для межсерверного взаимодействия, заданий по расписанию и запросов к самому сервису. - BASE_URL=http://omniroute:20128 - AUTH_COOKIE_SECURE=true caddy: image: caddy:latest container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" command: caddy reverse-proxy --from https://your-domain.com --to http://omniroute:20128 volumes: omniroute-data: ``` Caddy устанавливает стандартные заголовки переадресации для вышестоящего контейнера. OmniRoute использует `NEXT_PUBLIC_BASE_URL` как канонический публичный источник для обратных вызовов OAuth и создаваемых публичных ссылок; аутентифицированные операции записи из панели управления используют запросы того же источника и привязанную к сеансу защиту CSRF. Включайте `OMNIROUTE_TRUST_PROXY` только в сложных конфигурациях, где вы намеренно хотите, чтобы OmniRoute определял публичный источник по доверенным переадресованным заголовкам, а не по явной конфигурации. ## Быстрый туннель Cloudflare Поддержка панели управления для развёртываний Docker включает запускаемый одним щелчком **быстрый туннель Cloudflare** на странице `Панель управления → Конечные точки`. При первом включении `cloudflared` загружается только при необходимости, запускается временный туннель к вашей текущей конечной точке `/v1`, а созданный URL `https://*.trycloudflare.com/v1` отображается непосредственно под обычным публичным URL. Панели туннелей конечных точек (Cloudflare, Tailscale, ngrok) можно показывать или скрывать в разделе `Настройки → Внешний вид`, не изменяя состояние активного туннеля. ### Примечания о туннелях - URL быстрых туннелей являются временными и меняются после каждого перезапуска. - Быстрые туннели не восстанавливаются автоматически после перезапуска OmniRoute или контейнера. При необходимости повторно включите их в панели управления. - Управляемая установка в настоящее время поддерживает Linux, macOS и Windows на `x64` / `arm64`. - Управляемые быстрые туннели по умолчанию используют транспорт HTTP/2, чтобы избежать многочисленных предупреждений о размере UDP-буфера QUIC в контейнерных средах с ограниченными ресурсами. Задайте `CLOUDFLARED_PROTOCOL=quic` или `auto`, если хотите использовать другой транспорт. - Образы Docker включают системные корневые сертификаты центров сертификации и передают их управляемому `cloudflared`, что предотвращает ошибки доверия TLS при начальной настройке туннеля внутри контейнера. - Задайте `CLOUDFLARED_BIN=/absolute/path/to/cloudflared`, если хотите, чтобы OmniRoute использовал существующий исполняемый файл вместо его загрузки. ## Теги образов | Образ | Тег | Размер | Описание | | ------------------------ | -------- | ------ | ----------------------------------------------------------------------- | | `diegosouzapw/omniroute` | `latest` | ~250MB | Самая новая **опубликованная** стабильная версия SemVer (не git `main`) | | `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Закрепляйте теги этого типа для GitOps | Мультиплатформенный манифест: нативные `linux/amd64` + `linux/arm64` (Apple Silicon, AWS Graviton, Raspberry Pi). Docker автоматически выбирает подходящую архитектуру; передайте `--platform linux/amd64`, если необходимо принудительно использовать эмуляцию AMD64 на хостах ARM. ### Каналы выпусков OmniRoute публикует отдельные каналы Docker для стабильных выпусков, тестирования активной ветки выпуска и сборок для разработки. | Канал | Источник | Изменяемость | Рекомендуемое использование | | ------------------------------- | ------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `:` / `:-web` | Подписанный выпуск с версией | Неизменяемый | Рабочие развёртывания с закреплением точного выпуска | | `:latest` / `:latest-web` | Самая новая **опубликованная** стабильная версия SemVer | Изменяемый указатель на стабильную версию | Следует за стабильными выпусками **после** публикации SemVer — **не** отслеживает `main` или коммиты невыпущенных веток `release/v*` | | `:next` / `:next-web` | Текущая ветка `release/v*` по умолчанию | Изменяемый указатель на предварительный выпуск | Тестирование исправлений, уже добавленных в активную ветку выпуска, но ещё не вошедших в стабильный выпуск | | `:main` / `:main-web` | Ветка `main` | Изменяемый указатель на версию для разработки | Только разработка и интеграционное тестирование | #### Провайдеры веб-сеансов: образы `-web` Каждый из указанных выше каналов также доступен в виде тега `-web` (`:latest-web`, `:-web`, `:next-web`, `:main-web`), собранного на этапе `runner-web` — это тот же образ, но с Playwright и браузером Chromium. Обычный образ поставляется **без** Chromium; он необходим для `gemini-web`, `claude-web` и `claude-turnstile`. Ошибка возникает не при запуске, а позднее: эти провайдеры перечисляют свои модели и отображаются в панели управления как подключённые, и только первый запрос завершается ошибкой ``` [500]: Failed to load external module playwright: Error: Cannot find module '/app/node_modules/playwright/node_modules/playwright-core/browsers.json' ``` Если вы используете эти провайдеры, загрузите тег `-web` того канала, который уже используете, — больше ничего менять не нужно. При установке через npm/CLI (без образа Docker) недостающим компонентом будет исполняемый файл браузера: выполните `npx playwright install chromium` на хосте. #### Использование канала предварительных выпусков Канал `next` пересобирается при каждом push в текущую ветку по умолчанию `release/v*` и публикуется как для AMD64, так и для ARM64. Более старые ветки сопровождения не могут его перезаписать. Канал предоставляет доступный для загрузки образ с исправлениями, которые были объединены с активной веткой выпуска до создания следующего стабильного тега. ```bash docker pull diegosouzapw/omniroute:next docker pull diegosouzapw/omniroute:next-web ``` Для Docker Compose переопределите тег образа, используемый выбранным профилем, затем загрузите образ и пересоздайте сервис: ```yaml services: omniroute: image: diegosouzapw/omniroute:next ``` ```bash docker compose pull docker compose up -d ``` #### Безопасность и откат `next` — это плавающий канал предварительных выпусков. Он может изменяться при любом push в активную ветку выпуска и **не поддерживается для использования в рабочей среде**. При оценке конкретной сборки закрепите дайджест образа: ```bash docker pull diegosouzapw/omniroute:next docker image inspect diegosouzapw/omniroute:next --format '{{index .RepoDigests 0}}' ``` Перед тестированием создайте резервную копию тома данных OmniRoute или каталога данных, подключённого с помощью bind mount. Для отката восстановите ранее использовавшуюся стабильную версию или дайджест и пересоздайте контейнер: ```bash docker pull diegosouzapw/omniroute: docker compose up -d ``` Сборка из ветки выпуска никогда не может переместить `latest`; указатель стабильной версии может быть обновлён только подходящей стабильной версией с семантическим версионированием. Образы `next` сохраняют проверку образа выпуска и блокирующий шлюз для уязвимостей уровня CRITICAL. **`latest` не гарантирует актуальность относительно git.** Объединённые исправления в `main` или активной ветке `release/v*` **не** попадают в `:latest`, пока не будет опубликован стабильный образ SemVer и задание публикации не обновит `:latest` (с тем же дайджестом, что и у соответствующего SemVer). Если кажется, что `latest` не обновляется, хотя исправление уже отображается на GitHub, загрузите `:next`, чтобы протестировать ветку выпуска, или дождитесь тега SemVer. | Что вам нужно | Что использовать | | --------------------------------------------------------------------- | ---------------------------------------- | | GitOps / рабочая среда, где недопустимы непредвиденные изменения | Закрепите `:X.Y.Z` (или дайджест образа) | | Получать опубликованные стабильные версии и пересоздавать при выпуске | `:latest` | | Тестировать невыпущенные коммиты `release/v*` | `:next` (не для рабочей среды) | | Тестировать `main` | `:main` (не для рабочей среды) | ## Обновления панели управления в Docker Контейнер, запущенный из опубликованного образа, не может заменить сам себя. Когда OmniRoute обнаруживает, что работает внутри контейнера (без рабочей копии `.git` и не как npm-установка), баннер обновления на главной панели управления вместо инструкций npm/git показывает команду для запуска на хосте и кнопку копирования: ```bash docker compose pull omniroute && docker compose up -d --force-recreate omniroute ``` Имя сервиса берётся из `AUTO_UPDATE_SERVICE` (по умолчанию `omniroute` — сервис в `docker-compose.selfhost.yml`). `pull` обновляет только плавающий тег (`latest`, `next`); при закреплённом теге `:X.Y.Z` сначала измените тег. ### Обновление одним щелчком с помощью триггерного файла на хосте Чтобы кнопка **Обновить сейчас** продолжала работать без монтирования `/var/run/docker.sock` (которое предоставляет контейнеру root-доступ к хосту — см. предупреждение о профиле `cli` выше), задайте `DOCKER_UPDATE_TRIGGER_FILE` для сервиса OmniRoute и запустите наблюдатель на хосте: ```yaml services: omniroute: environment: DOCKER_UPDATE_TRIGGER_FILE: /app/data/update/docker-update.request volumes: - ./data:/app/data ``` Путь должен находиться внутри `DATA_DIR` (относительные значения разрешаются относительно него); все остальные пути отклоняются. После этого кнопка **Обновить сейчас** записывает `{ "requestedAt": "...", "targetVersion": "X.Y.Z" }` в этот файл, возвращает ответ `202`, а панель управления опрашивает `/api/system/version`, пока новая версия не будет запущена, после чего перезагружается. В этом режиме OmniRoute никогда не запускает команды оболочки или Docker. Как и для любого действия обновления, запрос принимается только от клиентов из loopback-интерфейса или локальной сети. Каталог данных, подключённый через bind mount, предоставляет хосту стабильный путь для наблюдения. Примеры юнитов `systemd` (замените `/srv/omniroute` на каталог, содержащий ваш compose-файл и `data/`): ```ini # /etc/systemd/system/omniroute-update.path [Unit] Description=Наблюдение за запросами обновления OmniRoute из панели управления [Path] PathExists=/srv/omniroute/data/update/docker-update.request [Install] WantedBy=multi-user.target ``` ```ini # /etc/systemd/system/omniroute-update.service [Unit] Description=Загрузка образа и пересоздание контейнера OmniRoute [Service] Type=oneshot WorkingDirectory=/srv/omniroute ExecStartPre=/usr/bin/rm -f /srv/omniroute/data/update/docker-update.request ExecStart=/bin/sh -c 'docker compose pull omniroute || (sleep 15 && docker compose pull omniroute)' ExecStart=/usr/bin/docker compose up -d --force-recreate omniroute ``` ```bash sudo systemctl daemon-reload sudo systemctl enable --now omniroute-update.path ``` Сервис удаляет запрос перед загрузкой образа, чтобы неудачный запуск не зациклился. Без `DOCKER_UPDATE_TRIGGER_FILE` панель управления сохраняет сценарий с ручным выполнением команды. ## Доступность: SQLite по умолчанию поддерживает только одну реплику Стандартная конфигурация OmniRoute для Docker / Kubernetes — это **один процесс Node + один процесс записи SQLite**. Высокая доступность в такой топологии **не поддерживается**. | Ограничение | Последствие | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Один процесс записи | **Не** запускайте несколько реплик с одним и тем же файлом SQLite. Это приведёт к повреждению БД. | | Пересоздание / перезапуск / завершение через HEALTHCHECK | **Полная недоступность** активных SSE-подключений, сеансов панели управления и состояния в памяти. Все подключённые клиенты отключаются. Новые запросы во время отсутствия доступных конечных точек получают от обратного прокси ошибку **`502 Bad Gateway: Unknown error`**, а не JSON от OmniRoute — клиенты не могут отличить её от сбоя провайдера (#11015). | | Тот же цикл событий, что и у `/healthz` | Загруженная операция с каталогом или цикл сжатия могут задержать проверки; при коротком тайм-ауте будет перезапущена **единственная** реплика. | **Матрица проверок** (см. также [рекомендации по проверкам Kubernetes](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations)): | Проверка | Цель | Не использовать | | ----------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | | Жизнеспособность | TCP на `PORT` (по умолчанию `20128`) или мягкая HTTP-проверка `/healthz` | `/api/monitoring/health` | | Готовность | HTTP `GET /healthz` | Короткие тайм-ауты, при которых загруженный цикл событий считается неработоспособным | | Углублённая / для людей | `/api/monitoring/health` | Автоматизированную проверку жизнеспособности kubelet | **Обновления:** ожидайте завершения всех сеансов. По возможности предварительно отключите клиентов; при использовании SQLite по умолчанию поэтапное обновление невозможно. Compose с `restart: unless-stopped` и Docker `HEALTHCHECK` также заменит единственный процесс, когда контейнер перейдёт в состояние Unhealthy, — с тем же масштабом последствий. Фрагмент конфигурации Kubernetes для **одной реплики** (требуется Recreate; не увеличивайте `replicas` при использовании одного файла SQLite): ```yaml spec: replicas: 1 strategy: type: Recreate template: spec: terminationGracePeriodSeconds: 90 containers: - name: omniroute lifecycle: preStop: exec: command: ["/bin/sleep", "15"] readinessProbe: httpGet: path: /healthz port: 20128 periodSeconds: 5 livenessProbe: tcpSocket: port: 20128 periodSeconds: 20 ``` Задержка `preStop` позволяет kube удалить конечные точки Service до SIGTERM, чтобы **новый** трафик перестал поступать умирающему процессу. Текущие SSE-соединения `/v1/responses` корректно завершаются в течение `SHUTDOWN_TIMEOUT_MS` (по умолчанию 30 с) с помощью тяжеловесных аренд допуска (#11015). Новые запросы, которые всё же достигают процесса, получают `503` + `Retry-After: 5`. Разрыв с отсутствием конечных точек при стратегии Recreate, пока замена не перейдёт в состояние Ready, остаётся полным простоем — это особенность топологии SQLite, а не неправильная настройка пробы. Внешний Postgres / HA с несколькими процессами записи **не** является документированным штатным вариантом. Если вам нужна HA, используйте одну реплику или топологию, которую проект отдельно протестировал и задокументировал. Работа над Postgres/MySQL ведётся в [#8075](https://github.com/diegosouzapw/OmniRoute/issues/8075). Пока она не завершена, единственный поддерживаемый способ увеличить пропускную способность для **крупных** `/v1/responses` — использовать N независимых процессов (см. следующий раздел), а не `replicas > 1` на одном томе. ## Горизонтальное масштабирование: N независимых процессов Один процесс Node — это **одна куча V8**. Два перекрывающихся запроса агента программирования `POST /v1/responses` объёмом ~3 МиБ / ~750 тыс. токенов (RTK + Caveman) аварийно завершают работу этой кучи примерно при 12 ГиБ (`FATAL ERROR: Reached heap limit`) и могут вызвать OOM в cgroup объёмом 16 ГиБ. См. [#7849](https://github.com/diegosouzapw/OmniRoute/issues/7849). Этот результат измерения — предупреждение о **бюджете памяти**, а не жёсткий продуктовый максимум в два одновременных длительных запроса `/v1/responses`. Допуск ресурсоёмких запросов чата ограничивается автоматически вычисляемым бюджетом байтов входящих данных (`OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES`, `src/shared/middleware/admissionBudget.ts`), рассчитанным на основе того же предела V8/cgroup. Повышение этого значения вручную (или установка устаревшего ограничения по количеству запросов `OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT`) для процесса с уже рассчитанными ресурсами снова приводит к аварийному завершению. Небольшие чаты, `/healthz`, `/v1/models` и MCP **не** входят в это ограничение. ### Один процесс: более двух длительных `/v1/responses` **Здоровый** процесс (использование кучи ниже `OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO`, по умолчанию `0.75`) **может** выполнять более двух одновременных длительных запросов `POST /v1/responses`, пока в общепроцессном бюджете байтов выполняющихся запросов (`OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES` / #10110) остаётся место. Тела запросов размером не менее `OMNIROUTE_CHAT_LARGE_BODY_BYTES` (по умолчанию 256 КиБ) получают ту же ресурсоёмкую аренду, что и запросы со сложной структурой, и используют тот же механизм обхода `tryAcquireHealthyHeadroom` из [#10437](https://github.com/diegosouzapw/OmniRoute/pull/10437) (`OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM`). Десятки одновременно подключённых длительных SSE-клиентов (операторам часто требуется 40–50) — это вопрос **бюджета памяти**: необходимо подобрать размеры кучи, основных и резервных слотов, а также `OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES`; жёсткого продуктового ограничения «максимум 2» нет. При высокой нагрузке на кучу запросы по-прежнему отклоняются с допускающим повторную попытку кодом `503`, чтобы проблема #7849 не возникла снова. Чтобы **умножить количество куч** (независимых старых пространств V8) **уже сейчас**: | Следует | Не следует | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | Запускать **N контейнеров/подов**, каждый со своим **собственным** `DATA_DIR` / томом | Задавать `replicas > 1` для одного файла SQLite | | Рассчитывать лимит ресурсоёмких выполняющихся запросов и резерв здорового состояния исходя из бюджета кучи / байтов выполняющихся запросов; 1–2 — консервативное значение по умолчанию из #7849, а не жёсткий максимум продукта | Выделять одному процессу в 8 раз больше ОЗУ и неограниченный лимит количества запросов | | При необходимости использовать `QUOTA_STORE_DRIVER=redis` + `QUOTA_STORE_REDIS_URL` для **общих счётчиков квот** | Считать Redis общей SQLite — это не так | | Дублировать секреты провайдеров в каждом экземпляре (либо согласиться с раздельными панелями мониторинга) | Ожидать единую панель мониторинга / единый журнал вызовов для всех экземпляров | | Разместить перед экземплярами любой балансировщик нагрузки; достаточно привязки по API-ключу или сеансу | Требовать зависящее от конкретного поставщика промежуточное ПО, учитывающее размер запросов | Аппаратные ресурсы: количество одновременно выполняемых длительных запросов `/v1/responses` на экземпляр — это вопрос **бюджета памяти** (куча + бюджет байтов выполняющихся запросов / #10110). `N` независимых `DATA_DIR` всё равно умножают количество куч: ОЗУ хоста должно покрывать `N × cgroup`, а не «один под на 16 ГиБ с N=8». Никогда не используйте `replicas > 1` для одного файла SQLite. Пример Compose (две кучи, два тома — не `deploy.replicas: 2`): ```yaml services: omniroute-a: image: diegosouzapw/omniroute:3.8.49 environment: DATA_DIR: /app/data OMNIROUTE_MEMORY_MB: "12288" QUOTA_STORE_DRIVER: redis QUOTA_STORE_REDIS_URL: redis://redis:6379 volumes: [omniroute-a-data:/app/data] ports: ["20128:20128"] omniroute-b: image: diegosouzapw/omniroute:3.8.49 environment: DATA_DIR: /app/data OMNIROUTE_MEMORY_MB: "12288" QUOTA_STORE_DRIVER: redis QUOTA_STORE_REDIS_URL: redis://redis:6379 volumes: [omniroute-b-data:/app/data] ports: ["20138:20128"] volumes: omniroute-a-data: omniroute-b-data: ``` Повышение плотности внутри процесса (вынесение сжатия из HTTP-изолята) отслеживается в [#11023](https://github.com/diegosouzapw/OmniRoute/issues/11023). Единый логический кластер на общем долговременном хранилище отслеживается в [#8075](https://github.com/diegosouzapw/OmniRoute/issues/8075). ## Региональные ошибки Gemini внутри Docker Google AI Studio / Gemini API может возвращать HTTP 400 с FAILED_PRECONDITION и сообщением `Местоположение пользователя не поддерживается для использования API.` Успешный запрос на хосте не означает, что контейнер использует тот же маршрут исходящего трафика. Порядок DNS, доступность IPv4/IPv6, маршрутизация VPN и настроенные прокси-серверы могут различаться. Проверьте [поддерживаемые Google регионы](https://ai.google.dev/gemini-api/docs/available-regions), а также фактический маршрут соединения; сама по себе эта ошибка не указывает на недействительный ключ API. ### Предпочтительно использовать прокси для конкретного соединения Используйте [настройку прокси для отдельного соединения](../ops/PROXY_GUIDE.md#4-level-proxy-system) в OmniRoute для затронутого соединения Gemini, затем повторите **Проверку соединения** и небольшой запрос с той же моделью. Так изменение маршрутизации будет ограничено этим соединением. Убедитесь, что прокси доступен из контейнера и что соединение действительно использует его. Изменение маршрута не гарантирует соответствия региональным требованиям вышестоящего сервиса. ### Сравните сетевое подключение хоста и контейнера При сравнении результатов аутентифицированных запросов используйте одинаковые ключ, модель и запрос; никогда не публикуйте в задаче учётные данные, пароли прокси или полные заголовки авторизации. Сначала проверьте, какие семейства адресов предоставляет системный резолвер, выполнив одну и ту же команду на хосте и внутри контейнера: ```bash node -e 'require("node:dns").lookup("generativelanguage.googleapis.com", {all: true}, (error, addresses) => { if (error) { console.error(error.code); process.exitCode = 1; return; } console.log(addresses.map(({family}) => family)); })' docker compose exec omniroute node -e 'require("node:dns").lookup("generativelanguage.googleapis.com", {all: true}, (error, addresses) => { if (error) { console.error(error.code); process.exitCode = 1; return; } console.log(addresses.map(({family}) => family)); })' ``` Замените `omniroute` на используемый вами сервис (например, `omniroute-web`). Эти команды выводят семейства адресов без учётных данных или IP-адресов. Возвращённое значение `6` показывает лишь наличие результата DNS для IPv6: оно **не** подтверждает наличие рабочего маршрута IPv6 или доступа к API. Если установлен `curl`, сравните `curl -4 -I https://generativelanguage.googleapis.com` с `curl -6 -I https://generativelanguage.googleapis.com` в обеих средах. Ответ HTTP подтверждает связность для этой проверки, даже если это ошибка запроса без аутентификации; только аутентифицированный запрос к модели проверяет доступность Gemini. ### Альтернатива на уровне хоста: рабочий IPv6 и политика резолвера Автор сообщения [#12762](https://github.com/diegosouzapw/OmniRoute/issues/12762) восстановил доступ в своей среде, включив IPv6 для контейнера и изменив выбор адресов в glibc. Рассматривайте это как альтернативу, зависящую от конкретной среды. Перед изменением предпочтений резолвера убедитесь в работоспособности IPv6 на хосте, исходящей связности и маршрутизации контейнера, а также правил брандмауэра. Само по себе наличие частного адреса ULA не подтверждает доступность публичного IPv6. Для сервисов, уже подключённых к сети Compose по умолчанию, следующий фрагмент включает IPv6 в этой сети; сохраните остальные настройки сервиса, портов, томов и конфигурации: ```yaml networks: default: enable_ipv6: true ``` Для именованной сети включите его в той сети, к которой фактически подключается сервис. Docker может выделить подсеть ULA; указывайте явную непересекающуюся подсеть только тогда, когда это требуется вашей сети. См. документацию по [работе Docker с сетью IPv6](https://docs.docker.com/engine/daemon/ipv6/) и [параметрам сетей Compose](https://docs.docker.com/reference/compose-file/networks/#enable_ipv6). В **образе на основе glibc** файл `/etc/gai.conf` может изменять выбор адресов. Текущий Dockerfile репозитория использует Debian; пользовательские образы на основе musl не поддерживают этот механизм. Указанное изменение меняет метку ULA с `label fc00::/7 6` на `label fc00::/7 1`. Возьмите за основу полную таблицу политик образа и сохраните остальные записи: добавление записи `label` или `precedence` заменяет соответствующую таблицу по умолчанию, поэтому файла, содержащего только изменённую строку, недостаточно. В [справочнике по конфигурации glibc](https://github.com/bminor/glibc/blob/master/posix/gai.conf) описана эта семантика. Подключите проверенный файл как доступный только для чтения по пути `/etc/gai.conf` и пересоздайте сервис, чтобы применить изменения. Это изменяет выбор адресов ОС для **всего исходящего трафика в этом контейнере**. Это не заставляет каждое приложение выбирать IPv6: порядок DNS и выбор соединения в Node также имеют значение. В частности, `--dns-result-order=ipv4first` отдаёт предпочтение IPv4 и не устраняет проблему, вызванную использованием только IPv4. См. документацию о [порядке DNS в Node](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder). После любых изменений на уровне хоста повторно проверьте Gemini и других поставщиков. Для отката удалите пользовательское подключение `gai.conf`, восстановите прежнюю конфигурацию сети и пересоздайте затронутые сервис и сеть во время окна технического обслуживания. Пересоздание сети может прервать работу других подключённых к ней контейнеров; не удаляйте том с постоянными данными. ## Важные примечания - **Режим SQLite WAL:** необходимо дождаться завершения `docker stop`, чтобы OmniRoute успел выполнить контрольную точку и записать последние изменения обратно в `storage.sqlite`. Включённые файлы Compose уже задают 40-секундный льготный период остановки. Если вы запускаете образ напрямую, используйте `--stop-timeout 40`. - **`DISABLE_SQLITE_AUTO_BACKUP`:** установите значение `true`, если регулярные резервные копии и резервные копии перед записью создаются внешними средствами. Для миграций существующей базы данных по-прежнему требуются отдельный надёжный снимок для обеспечения безопасности и защита от массовой миграции. - **Сохранность данных:** всегда подключайте том к `/app/data`, чтобы сохранять базу данных, ключи и конфигурации при перезапусках контейнера. - **Настройка порта:** переопределите переменную окружения `PORT`, чтобы изменить порт `20128`, используемый по умолчанию. ## См. также - [Руководство по развёртыванию на виртуальной машине](../ops/VM_DEPLOYMENT_GUIDE.md) — настройка виртуальной машины + nginx + Cloudflare - [Руководство по развёртыванию на Fly.io](../ops/FLY_IO_DEPLOYMENT_GUIDE.md) — развёртывание на Fly.io - [Настройка окружения](../reference/ENVIRONMENT.md) — полное справочное описание `.env`