> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Развёртывание

> Топология compose сервис за сервисом, что обязано сохраняться, как применяются миграции и как обновляться и откатываться

Omniflow поставляется как один стек Docker Compose для одного Linux-сервера: PostgreSQL, Valkey, одноразовая задача миграции, три процесса на Go (`api`, `bot`, `worker`) и необязательный контейнер `web` на Next.js за профилем compose. `compose.yaml` в корне репозитория — это вся топология целиком: ни Helm-чарта, ни оператора, ни второй поддерживаемой формы нет.

Сервисы с состоянием остаются приватными для сети compose, и Omniflow не поставляет обратный прокси, потому что TLS, ограничения на размер тела и доверие к проброшенным заголовкам — это решения, которые оператор уже принимает для собственной инфраструктуры. Два поддерживаемых примера точки входа лежат в `deploy/proxies/`; см. [Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).

## Топология, сервис за сервисом

| Сервис | Образ или сборка | Опубликованные порты | Чего ждёт | Тома |
| - | - | - | - | - |
| `postgres` | `postgres:18.4-alpine` | нет | — | `postgres-data:/var/lib/postgresql` |
| `valkey` | `valkey/valkey:9.1.1-alpine` | нет | — | нет, намеренно |
| `migrate` | `arigaio/atlas:1.3.0` | — | здоровья `postgres` | `./database/migrations`, смонтирован только на чтение |
| `api` | `build/go.Dockerfile`, `TARGET=api` | `8080:8080` | завершения `migrate`, здоровья `valkey` | нет |
| `bot` | `build/go.Dockerfile`, `TARGET=bot`, `RUNTIME=postgres` | нет | того же | `backup-data:/var/lib/omniflow/backups` |
| `worker` | `build/go.Dockerfile`, `TARGET=worker`, `RUNTIME=postgres` | нет | того же | `backup-data:/var/lib/omniflow/backups` |
| `web` | `build/web.Dockerfile`, профиль `web` | `3000:3000` | `api` | нет |

Порт на хосте публикуют только `api` и `web`. PostgreSQL и Valkey доступны исключительно во внутренней сети — именно поэтому ничему в стеке не нужен пароль базы данных, который вам было бы не страшно выставить наружу.

У `postgres` есть healthcheck `pg_isready` каждые 5 секунд с 20 повторами, у `valkey` — healthcheck `valkey-cli ping`, а сервисы приложения зависят от того, что `migrate` *успешно завершился*, а не просто стартовал. Схема, которая не применилась, поэтому останавливает релиз, а не порождает три процесса, работающих против наполовину смигрированной базы.

Каждый из `api`, `bot` и `worker` читает `env_file: [.env]`, так что все три видят одну и ту же конфигурацию. API дополнительно получает `APP_HTTP_ADDR: :8080`, заданный явно в compose. Что делает каждая переменная, каталогизировано в [Конфигурации](https://omniflow-vpn.mintlify.site/ru/getting-started/configuration.md).

### Почему `valkey` настроен без персистентности

Команда — `valkey-server --save "" --appendonly no`: ни снимков, ни файла дозаписи. Valkey держит записи кеша, счётчики ограничения частоты запросов, блокировки и одноразовые токены защиты от повторов и больше ничего, поэтому его потеря стоит окна состояния ограничителя, а не данных. Всё, что Omniflow не может позволить себе потерять, лежит в PostgreSQL.

> [!WARNING]
>
> Следствие в том, что перезапуск Valkey очищает защиты от повторов, охраняющие платёжные колбэки и разрушительные кнопки Telegram, на всю длину их окна хранения. Перезапускайте его в спокойный период, а не в середине рассылки.

## Что обязано сохраняться

Состояние несут два именованных тома:

* **`postgres-data`** — всё. Заказы, книга проводок, права на услугу, события аудита, сессии, настройки, очередь. Том смонтирован в `/var/lib/postgresql` — каталог, который объявляет сам образ, — а не в `/var/lib/postgresql/data`, который был `PGDATA` вплоть до PostgreSQL 17. В версии 18 он переехал в `/var/lib/postgresql/18/docker`, чтобы мажорное обновление могло держать два каталога данных рядом; монтирование старого пути оставляет этот том пустым, а база уходит в анонимный том, который рано или поздно удалит `docker volume prune`. Если вы обновляете установку, работавшую со старым монтированием, ваши данные в этом анонимном томе: найдите его через `docker volume ls -f dangling=true` или снимите резервную копию до смены монтирования и восстановите её после.
* **`backup-data`** — зашифрованный вывод `pg_dump`, смонтированный в `/var/lib/omniflow/backups` и на `bot`, и на `worker`, потому что один делает резервные копии по расписанию, а другой — по требованию. Резервные копии живут вне файловой системы контейнера, так что пересборка их никогда не выбрасывает.

Если вы меняете, куда смонтирован PostgreSQL — другой compose-файл, bind-монтирование, возвращение управляемой базы обратно к себе, — проверьте результат, а не предполагайте его:

```bash theme={null}
sh tools/volume-drill.sh
```

Он работает под собственным compose-проектом, поэтому никогда не трогает стек, уже поднятый на этом хосте. Он проверяет, что работающий `data_directory` находится внутри именованного тома и что у контейнера нет анонимных томов, затем пишет строку, пересоздаёт контейнер и читает её обратно. CI прогоняет его против поставляемого `compose.yaml` на каждое изменение.

Вложения к обращениям поддержки, загруженные через клиентскую веб-панель, — то исключение, с которым вам придётся разобраться самостоятельно. Они пишутся в `APP_SUPPORT_ATTACHMENT_DIR`, который по умолчанию равен `/var/lib/omniflow/support-attachments`, а `compose.yaml` **не объявляет для этого пути тома ни на `api`, ни на `worker`**. Как поставляется, загруженное из браузера вложение живёт в записываемом слое контейнера API и исчезает при следующей пересборке, а воркер — процесс, который освобождает осиротевшие файлы вложений, — не видит тот же каталог. Если ваша установка принимает загрузки из веба, добавьте общий том, смонтированный по одному и тому же пути на обоих сервисах, и задайте `APP_SUPPORT_ATTACHMENT_DIR` на обоих. Воркер, направленный в другой каталог, удаляет строки и оставляет байты.

## Первое развёртывание

**1. Подготовьте файл окружения**

```bash theme={null}
cp .env.example .env
```

Заполните `APP_DATABASE_URL`, `APP_VALKEY_URL`, `APP_REMNAWAVE_URL`, `APP_REMNAWAVE_TOKEN`, `APP_OPERATOR_TOKEN`, 32-байтный `APP_DATA_ENCRYPTION_KEY` в base64 и `APP_TELEGRAM_TOKEN`, если вы запускаете бота. API отказывается стартовать, когда задан URL базы данных, а любая из первых пяти переменных отсутствует, и завершается с сообщением `commerce requires APP_OPERATOR_TOKEN, APP_DATA_ENCRYPTION_KEY, APP_REMNAWAVE_URL, APP_REMNAWAVE_TOKEN, and APP_VALKEY_URL`.

**2. Поднимите основной стек**

```bash theme={null}
docker compose up --build postgres valkey migrate api bot worker
```

`make dev` запускает тот же набор. Добавьте веб-приложение командой `docker compose --profile web up --build` или `make up`.

**3. Прочитайте токен первичной настройки из лога API**

```
level=WARN msg="no administrator exists yet; redeem this one-time setup token at /admin/setup" token=… expires_in=30m0s
```

Погасите его на `/admin/setup` в течение 30 минут, чтобы создать первого владельца. См. [Панель администратора](https://omniflow-vpn.mintlify.site/ru/operations/admin-panel.md).

**4. Проверьте готовность до того, как направлять трафик**

```bash theme={null}
curl -s localhost:8080/readyz
```

Пропуск — это `200` со `"status":"ready"` и здоровой каждой проверкой. Что означают пробы и единственный случай, когда зелёный `/readyz` вводит в заблуждение, разобрано в [Наблюдаемости](https://omniflow-vpn.mintlify.site/ru/operations/observability.md).

После этого API доступен на `http://localhost:8080`, клиентское приложение аккаунта — на `http://localhost:3000`, а рабочее место оператора — на `http://localhost:3000/admin`.

## Применение миграций

Миграции — это SQL-файлы только вперёд в `database/migrations/`, применяемые Atlas. Сервис `migrate` выполняет `atlas migrate apply` против базы данных и завершается; Atlas записывает уже применённое в схему `atlas_schema_revisions`, поэтому повторный запуск на актуальной базе ничего не делает, и весь стек можно поднимать сколько угодно раз без особой обработки.

Обратных миграций нет. Это осознанный выбор, а не упущение: откат схемы — это восстановление из резервной копии, единственный путь, который заодно восстанавливает и данные, которые прямая миграция переформовала. См. [Резервное копирование и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

> [!NOTE]
>
> Сервис `migrate` жёстко зашивает `postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable` в свою команду и **не** читает `.env`. Смена учётных данных базы данных означает правку `compose.yaml` наряду с `APP_DATABASE_URL`, иначе задача миграции продолжит указывать на старые.

Те же миграции создают таблицы долговременной очереди задач, поэтому первому развёртыванию не нужна отдельная настройка очереди; см. [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#схема-очереди-поставляется-вместе-с-миграциями).

О том, как миграции пишутся и рецензируются, см. [Базу данных](https://omniflow-vpn.mintlify.site/ru/contributing/database.md).

## Образы

`build/go.Dockerfile` собирает все три бинарника на Go из одного многоступенчатого файла. `CGO_ENABLED=0 go build -trimpath -ldflags="-s -w"` даёт статический бинарник, а аргумент сборки `RUNTIME` выбирает финальную базу:

| `RUNTIME` | База | Для чего используется |
| - | - | - |
| `static` (по умолчанию) | `gcr.io/distroless/static-debian13:nonroot` | `api` |
| `postgres` | `alpine:3.23.5` с `postgresql18-client`, `ca-certificates`, `tzdata` | `bot`, `worker` |

`pg_dump` и `pg_restore` обязаны совпадать по мажорной версии с сервером — поэтому клиент зафиксирован на PostgreSQL 18 рядом с сервером `postgres:18.4-alpine`. Alpine 3.23 — первый выпуск, несущий `postgresql18-client`, так что понижение этой базы ломает путь резервного копирования.

Обе базы работают под `nonroot` (uid 65532 в случае Alpine), а `/var/lib/omniflow` создан заранее и принадлежит этому пользователю, так что каталог резервных копий доступен на запись без привилегированного контейнера.

Образ web устанавливает зависимости и собирается с помощью Bun, а затем отдаёт standalone-вывод Next.js из `node:26.7-alpine` на порту 3000. npm из рантайм-стадии удалён: там его никто не вызывает, а приходит он с базовым образом и несёт собственное дерево зависимостей, которое скан образа показывает как находки. Удаление убирает код, а не заглушает сканер.

### Выпущенные образы

Тег `v*` собирает и публикует `api`, `bot`, `worker` и `web` в `ghcr.io/<repo>/<name>` для `linux/amd64` и `linux/arm64` с тегами `{{version}}` и `{{major}}.{{minor}}`, с полным provenance и SBOM.

Ничто в репозитории эти образы не потребляет: `compose.yaml` собирает из рабочего дерева. Запуск закреплённого релиза означает написание небольшого override, который заменяет каждый блок `build:` строкой `image:`. Не направляйте продакшен на плавающий тег — фиксируйте полную версию, чтобы перезапускалось именно то, что вы протестировали.

> [!WARNING]
>
> Релизный workflow передаёт в сборку только `TARGET`, поэтому опубликованные образы `bot` и `worker` используют базу `static` по умолчанию и **не** содержат ни `pg_dump`, ни `pg_restore`. Резервным копиям по расписанию и поверхности восстановления `/ops` нужны образы, собранные с `RUNTIME=postgres`, — именно так делает `compose.yaml`. Если вы переходите на выпущенные образы, продолжайте собирать `bot` и `worker` сами, иначе резервное копирование будет падать с ошибкой команды.

## Поддерживаемая топология

| Роль | Реплик | Почему |
| - | - | - |
| `api` | Сколько угодно, за прокси | Он не выполняет фоновых проходов, меняющих состояние. Его единственный такой цикл — контроллер режима обслуживания — читает текущее состояние обслуживания, прежде чем его менять, поэтому вторая реплика застаёт переход уже сделанным |
| `worker` | Сколько угодно | River распределяет задачи между репликами, а периодические проходы — обслуживание заказов и корзин, сверку платежей, продления, зачистки, кампании, очистку по срокам хранения, резервные копии — ставит в очередь только выбранный River лидер, поэтому каждый выполняется один раз за интервал на всю установку |
| `bot` | Ровно один | Telegram доставляет обновления одному потребителю; long polling отказывает второму `getUpdates` |

Нескольким воркерам нужно то же общее хранилище, что и одному: `APP_BACKUP_DIR` на общем томе, потому что резервная копия оказывается на той реплике, которая выполнила проход, и тот же `APP_SUPPORT_ATTACHMENT_DIR`, что у API. Каждая реплика открывает собственный пул подключений к базе, поэтому каждая расходует `max_connections` PostgreSQL — см. ниже. Полный перечень проходов и внутрипроцессных циклов — в [Задачах и очередях](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#периодические-проходы-и-внутрипроцессные-циклы).

### Подключения к базе данных

Каждый процесс ограничивает свой пул PostgreSQL переменной `APP_DATABASE_MAX_CONNS` — целым положительным числом; с любым другим значением процесс не запускается. Если она не задана:

| Процесс | Пул по умолчанию |
| - | - |
| `api` | 20 |
| `bot` | 10 |
| `worker` | 40 |

В сумме значения по умолчанию дают 70 — в пределах стандартного `max_connections` PostgreSQL, равного 100, с запасом для миграций, резервных копий и `psql` оператора. Воркер отказывается запускаться с пулом меньше 32 подключений: он выполняет до 28 задач одновременно во всех очередях плюс 4 подключения на служебную работу самого River. Параметр `pool_max_conns` в `APP_DATABASE_URL` имеет приоритет над переменной.

Значение в общем `.env` применяется ко всем трём процессам. Чтобы задать им разные размеры, укажите переменную для каждого сервиса в `environment:` в override-файле compose. Добавляя реплики, сложите пулы всех реплик и держите сумму ниже `max_connections`.

## Как выставить стек наружу

С базовым файлом compose API отвечает на порту 8080 хоста, а веб-приложение — на 3000, что верно для локального запуска и неверно для интернета. Поставьте перед ними прокси:

```bash theme={null}
ACME_EMAIL=ops@example.com \
WEB_DOMAIN=app.example.com \
API_DOMAIN=api.example.com \
BOT_WEBHOOK_DOMAIN=bot.example.com \
docker compose -f compose.yaml -f deploy/proxies/traefik.compose.yaml up -d
```

Оверлей использует `ports: !override []`, чтобы `api` и `web` вообще перестали публиковаться на хосте; для этого нужен Docker Compose 2.24.4 или новее, а на более старом выпуске удалите эти блоки `ports` из `compose.yaml` вручную. Пример на Caddy в том же каталоге делает эквивалентную работу.

Оба примера отдают 404 на `/metrics` и `/readyz` на публичном хосте API, ограничивают размер тела запроса и отказываются доверять `X-Forwarded-For` откуда-либо, кроме приватных диапазонов. Задайте `APP_TRUSTED_PROXIES` соответственно, иначе журнал аудита и ограничитель частоты запросов будут видеть адрес прокси вместо адреса клиента. Подробности, включая то, почему прокси, перекодирующий тела JSON, ломает подпись каждого вебхука провайдера, — в [Обратном прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).

## Обновление

**1. Сделайте резервную копию и убедитесь, что она проверена**

Каждая резервная копия Omniflow расшифровывается и хешируется от начала до конца, прежде чем быть помеченной как `completed`, поэтому строка `completed` — это копия, читаемость которой уже доказана. Не обновляйтесь, опираясь на копию в состоянии `running`. См. [Резервное копирование и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

**2. Прочитайте примечания к релизу на предмет веса миграций**

Миграции выполняются до старта новых контейнеров, и стек их ждёт. Миграция, переписывающая большую таблицу, держит весь релиз открытым всё время своей работы.

**3. Приостановите покупки, если обновление затрагивает выдачу доступа**

Включение режима обслуживания останавливает новые заказы и удерживает исходящие вызовы Remnawave, не трогая уже принятые деньги. Удержанные задачи выдачи доступа откладываются, а не проваливаются, и возобновляются, когда вы снимете режим. См. [Режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md).

**4. Обновите исходники или закреплённые теги образов, затем поднимите стек**

```bash theme={null}
docker compose up -d --build migrate api bot worker
```

Сначала до конца отрабатывает `migrate`; затем три процесса стартуют против новой схемы.

**5. Проверьте, затем снимите режим обслуживания**

Проверьте `/readyz` на API и на `APP_WORKER_HTTP_ADDR` воркера, убедитесь, что воркер залогировал `worker started`, затем закройте окно обслуживания с указанием причины.

Конфигурация читается один раз при старте и никогда не перечитывается, поэтому изменённому `.env` нужен перезапуск затронутого процесса. Ничто в Omniflow не следит за окружением или файлом конфигурации, и разделы настроек в операторской панели тоже не питают рантайм — см. [Настройки установки](https://omniflow-vpn.mintlify.site/ru/operations/installation-settings.md).

### Поведение при завершении во время перезапуска

Все три процесса устанавливают обработчик `SIGINT`/`SIGTERM` и завершаются с бюджетом в 15 секунд. API перестаёт принимать соединения и даёт запросам в полёте завершиться, прежде чем закрыть свои пулы; воркер закрывает операционный слушатель и затем вызывает `Stop` у River, который дожидается завершения выполняющихся задач, так что перезапуск никогда не бросает прогон выдачи доступа на полпути; бот таким же образом закрывает свои вебхуковый и операционный серверы.

Периодические проходы — это задачи River, и они останавливаются вместе с River. Оставшиеся внутрипроцессные циклы-таймеры выходят по тому же контексту, и их никто не дожидается. Проходу, который ещё выполняется, когда 15-секундный запас исчерпан, отменяют контекст, поэтому резервная копия, прерванная посреди `pg_dump`, записывается как неуспешная с `cancelled`, а не остаётся в `running`.

> [!NOTE]
>
> Ни один из основных сервисов compose не объявляет политику `restart:` — её задаёт только оверлей Traefik. Завершившийся процесс останется лежать, пока кто-нибудь его не запустит. Добавьте `restart: unless-stopped` в override, если хотите, чтобы стек самостоятельно переживал перезагрузку хоста.

## Откат

Пути вниз по схеме нет, поэтому откат принимает одну из двух форм в зависимости от того, что изменилось.

**Миграция не выполнялась.** Верните предыдущий тег образа или ревизию исходников и поднимите стек. Больше ничего не нужно.

**Миграция выполнялась.** Старые бинарники не понимают новую схему, поэтому восстановление базы данных — часть отката. Восстановите резервную копию, сделанную до обновления, и запустите против неё предыдущую версию. `pg_restore` работает с `--clean --if-exists --single-transaction`, поэтому всё записанное после точки резервной копии теряется — именно поэтому шаг с резервной копией предшествует обновлению, а не следует за ним.

> [!WARNING]
>
> Восстановление в пустую базу данных обязано нести и схему `atlas_schema_revisions`, иначе Atlas при следующем старте попытается применить каждую миграцию заново. Полный дамп в custom-формате её содержит; выборочно отфильтрованный может и не содержать.

Сам путь восстановления — поверхность `/ops` в Telegram и отдельный фильтр `--decrypt-backup`, которому не нужно ничего, кроме бинарника воркера, ключа и файла, — описан в [Резервном копировании и восстановлении](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

## Куда смотреть, когда развёртывание ведёт себя не так

* Процесс сразу завершился с `invalid configuration` — обязательная переменная отсутствует или задана неверно; сообщение называет нарушенное правило.
* `migrate` упал, и сервисы приложения так и не стартовали — читайте его лог; стек спроектирован останавливаться здесь.
* `/readyz` возвращает 503 — одна зависимость отказала; тело ответа называет какая, с классификацией `timeout` или `unavailable`. См. [Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md).
* Покупки отклоняются с `maintenance_active` — автоопределение открыло окно. См. [Режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md).
* Более широкий разбор от симптома — в [Диагностике неисправностей](https://omniflow-vpn.mintlify.site/ru/operations/troubleshooting.md).


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