api, bot, worker) и необязательный контейнер web на Next.js за профилем compose. compose.yaml в корне репозитория — это вся топология целиком: ни Helm-чарта, ни оператора, ни второй поддерживаемой формы нет.
Сервисы с состоянием остаются приватными для сети compose, и Omniflow не поставляет обратный прокси, потому что TLS, ограничения на размер тела и доверие к проброшенным заголовкам — это решения, которые оператор уже принимает для собственной инфраструктуры. Два поддерживаемых примера точки входа лежат в deploy/proxies/; см. Обратный прокси.
Топология, сервис за сервисом
Порт на хосте публикуют только
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. Что делает каждая переменная, каталогизировано в Конфигурации.
Почему valkey настроен без персистентности
Команда — valkey-server --save "" --appendonly no: ни снимков, ни файла дозаписи. Valkey держит записи кеша, счётчики ограничения частоты запросов, блокировки и одноразовые токены защиты от повторов и больше ничего, поэтому его потеря стоит окна состояния ограничителя, а не данных. Всё, что Omniflow не может позволить себе потерять, лежит в PostgreSQL.
Что обязано сохраняться
Состояние несут два именованных тома: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, потому что один делает резервные копии по расписанию, а другой — по требованию. Резервные копии живут вне файловой системы контейнера, так что пересборка их никогда не выбрасывает.
data_directory находится внутри именованного тома и что у контейнера нет анонимных томов, затем пишет строку, пересоздаёт контейнер и читает её обратно. CI прогоняет его против поставляемого compose.yaml на каждое изменение.
Вложения к обращениям поддержки, загруженные через клиентскую веб-панель, — то исключение, с которым вам придётся разобраться самостоятельно. Они пишутся в APP_SUPPORT_ATTACHMENT_DIR, который по умолчанию равен /var/lib/omniflow/support-attachments, а compose.yaml не объявляет для этого пути тома ни на api, ни на worker. Как поставляется, загруженное из браузера вложение живёт в записываемом слое контейнера API и исчезает при следующей пересборке, а воркер — процесс, который освобождает осиротевшие файлы вложений, — не видит тот же каталог. Если ваша установка принимает загрузки из веба, добавьте общий том, смонтированный по одному и тому же пути на обоих сервисах, и задайте APP_SUPPORT_ATTACHMENT_DIR на обоих. Воркер, направленный в другой каталог, удаляет строки и оставляет байты.
Первое развёртывание
1
Подготовьте файл окружения
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
Поднимите основной стек
make dev запускает тот же набор. Добавьте веб-приложение командой docker compose --profile web up --build или make up.3
Прочитайте токен первичной настройки из лога API
/admin/setup в течение 30 минут, чтобы создать первого владельца. См. Панель администратора.4
Проверьте готовность до того, как направлять трафик
200 со "status":"ready" и здоровой каждой проверкой. Что означают пробы и единственный случай, когда зелёный /readyz вводит в заблуждение, разобрано в Наблюдаемости.http://localhost:8080, клиентское приложение аккаунта — на http://localhost:3000, а рабочее место оператора — на http://localhost:3000/admin.
Применение миграций
Миграции — это SQL-файлы только вперёд вdatabase/migrations/, применяемые Atlas. Сервис migrate выполняет atlas migrate apply против базы данных и завершается; Atlas записывает уже применённое в схему atlas_schema_revisions, поэтому повторный запуск на актуальной базе ничего не делает, и весь стек можно поднимать сколько угодно раз без особой обработки.
Обратных миграций нет. Это осознанный выбор, а не упущение: откат схемы — это восстановление из резервной копии, единственный путь, который заодно восстанавливает и данные, которые прямая миграция переформовала. См. Резервное копирование и восстановление.
Сервис
migrate жёстко зашивает postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable в свою команду и не читает .env. Смена учётных данных базы данных означает правку compose.yaml наряду с APP_DATABASE_URL, иначе задача миграции продолжит указывать на старые.Образы
build/go.Dockerfile собирает все три бинарника на Go из одного многоступенчатого файла. CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" даёт статический бинарник, а аргумент сборки RUNTIME выбирает финальную базу:
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:. Не направляйте продакшен на плавающий тег — фиксируйте полную версию, чтобы перезапускалось именно то, что вы протестировали.
Поддерживаемая топология
Нескольким воркерам нужно то же общее хранилище, что и одному:
APP_BACKUP_DIR на общем томе, потому что резервная копия оказывается на той реплике, которая выполнила проход, и тот же APP_SUPPORT_ATTACHMENT_DIR, что у API. Каждая реплика открывает собственный пул подключений к базе, поэтому каждая расходует max_connections PostgreSQL — см. ниже. Полный перечень проходов и внутрипроцессных циклов — в Задачах и очередях.
Подключения к базе данных
Каждый процесс ограничивает свой пул PostgreSQL переменнойAPP_DATABASE_MAX_CONNS — целым положительным числом; с любым другим значением процесс не запускается. Если она не задана:
В сумме значения по умолчанию дают 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, что верно для локального запуска и неверно для интернета. Поставьте перед ними прокси:ports: !override [], чтобы api и web вообще перестали публиковаться на хосте; для этого нужен Docker Compose 2.24.4 или новее, а на более старом выпуске удалите эти блоки ports из compose.yaml вручную. Пример на Caddy в том же каталоге делает эквивалентную работу.
Оба примера отдают 404 на /metrics и /readyz на публичном хосте API, ограничивают размер тела запроса и отказываются доверять X-Forwarded-For откуда-либо, кроме приватных диапазонов. Задайте APP_TRUSTED_PROXIES соответственно, иначе журнал аудита и ограничитель частоты запросов будут видеть адрес прокси вместо адреса клиента. Подробности, включая то, почему прокси, перекодирующий тела JSON, ломает подпись каждого вебхука провайдера, — в Обратном прокси.
Обновление
1
Сделайте резервную копию и убедитесь, что она проверена
Каждая резервная копия Omniflow расшифровывается и хешируется от начала до конца, прежде чем быть помеченной как
completed, поэтому строка completed — это копия, читаемость которой уже доказана. Не обновляйтесь, опираясь на копию в состоянии running. См. Резервное копирование и восстановление.2
Прочитайте примечания к релизу на предмет веса миграций
Миграции выполняются до старта новых контейнеров, и стек их ждёт. Миграция, переписывающая большую таблицу, держит весь релиз открытым всё время своей работы.
3
Приостановите покупки, если обновление затрагивает выдачу доступа
Включение режима обслуживания останавливает новые заказы и удерживает исходящие вызовы Remnawave, не трогая уже принятые деньги. Удержанные задачи выдачи доступа откладываются, а не проваливаются, и возобновляются, когда вы снимете режим. См. Режим обслуживания.
4
Обновите исходники или закреплённые теги образов, затем поднимите стек
migrate; затем три процесса стартуют против новой схемы.5
Проверьте, затем снимите режим обслуживания
Проверьте
/readyz на API и на APP_WORKER_HTTP_ADDR воркера, убедитесь, что воркер залогировал worker started, затем закройте окно обслуживания с указанием причины..env нужен перезапуск затронутого процесса. Ничто в Omniflow не следит за окружением или файлом конфигурации, и разделы настроек в операторской панели тоже не питают рантайм — см. Настройки установки.
Поведение при завершении во время перезапуска
Все три процесса устанавливают обработчикSIGINT/SIGTERM и завершаются с бюджетом в 15 секунд. API перестаёт принимать соединения и даёт запросам в полёте завершиться, прежде чем закрыть свои пулы; воркер закрывает операционный слушатель и затем вызывает Stop у River, который дожидается завершения выполняющихся задач, так что перезапуск никогда не бросает прогон выдачи доступа на полпути; бот таким же образом закрывает свои вебхуковый и операционный серверы.
Периодические проходы — это задачи River, и они останавливаются вместе с River. Оставшиеся внутрипроцессные циклы-таймеры выходят по тому же контексту, и их никто не дожидается. Проходу, который ещё выполняется, когда 15-секундный запас исчерпан, отменяют контекст, поэтому резервная копия, прерванная посреди pg_dump, записывается как неуспешная с cancelled, а не остаётся в running.
Ни один из основных сервисов compose не объявляет политику
restart: — её задаёт только оверлей Traefik. Завершившийся процесс останется лежать, пока кто-нибудь его не запустит. Добавьте restart: unless-stopped в override, если хотите, чтобы стек самостоятельно переживал перезагрузку хоста.Откат
Пути вниз по схеме нет, поэтому откат принимает одну из двух форм в зависимости от того, что изменилось. Миграция не выполнялась. Верните предыдущий тег образа или ревизию исходников и поднимите стек. Больше ничего не нужно. Миграция выполнялась. Старые бинарники не понимают новую схему, поэтому восстановление базы данных — часть отката. Восстановите резервную копию, сделанную до обновления, и запустите против неё предыдущую версию.pg_restore работает с --clean --if-exists --single-transaction, поэтому всё записанное после точки резервной копии теряется — именно поэтому шаг с резервной копией предшествует обновлению, а не следует за ним.
Сам путь восстановления — поверхность /ops в Telegram и отдельный фильтр --decrypt-backup, которому не нужно ничего, кроме бинарника воркера, ключа и файла, — описан в Резервном копировании и восстановлении.
Куда смотреть, когда развёртывание ведёт себя не так
- Процесс сразу завершился с
invalid configuration— обязательная переменная отсутствует или задана неверно; сообщение называет нарушенное правило. migrateупал, и сервисы приложения так и не стартовали — читайте его лог; стек спроектирован останавливаться здесь./readyzвозвращает 503 — одна зависимость отказала; тело ответа называет какая, с классификациейtimeoutилиunavailable. См. Наблюдаемость.- Покупки отклоняются с
maintenance_active— автоопределение открыло окно. См. Режим обслуживания. - Более широкий разбор от симптома — в Диагностике неисправностей.