> ## 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.

# Резервные копии, восстановление и аварийное восстановление

> Зашифрованные резервные копии PostgreSQL, проверенные восстановления, обращение с ключами, а также процедура обновления и отката

PostgreSQL — единственное долговременное хранилище Omniflow. Valkey держит кэш,
ограничения частоты запросов, блокировки и одноразовое состояние, поэтому его потеря не
стоит ничего, кроме прогретого кэша. Всё на этой странице поэтому касается PostgreSQL.

## Что такое резервная копия

Резервная копия — это вывод `pg_dump --format=custom --no-owner --no-privileges --compress=6`,
зашифрованный AES-256-GCM и записанный в `APP_BACKUP_DIR`. Omniflow вызывает
`pg_dump` и `pg_restore` как внешние программы, а не переписывает их заново, поэтому
резервную копию всегда можно восстановить штатными средствами PostgreSQL, даже если сам
Omniflow не запускается.

Шифрование кадровое, а не один запечатанный блок, поэтому многогигабайтный дамп никогда не
обязан помещаться в память:

```text theme={null}
"OFBK1"          5 bytes
base nonce      12 bytes, random per file
frames                    repeated: uint32 length, AES-256-GCM ciphertext
```

Размер кадра — 1 MiB. Nonce каждого кадра — это базовый nonce XOR его индекса, и каждый кадр
аутентифицирует собственный индекс и признак того, завершает ли он файл. Переставленный,
продублированный, потерянный или обрезанный кадр не открывается вместо того, чтобы выдать
правдоподобно выглядящий частичный дамп, — а это единственный исход хуже, чем полное
отсутствие резервной копии. Файл, который не является резервной копией Omniflow, отвергается
уже на магических байтах.

Файлы называются `omniflow-<kind>-<UTC timestamp>.dump.enc`, например
`omniflow-scheduled-20260811T030000Z.dump.enc`. `kind` — это `scheduled` или `manual` и ничего
больше: на столбце стоит check-ограничение, поэтому третий вид потребует миграции. Имя
уникально, а файл создаётся эксклюзивно, поэтому коллизия приводит к ошибке, а не к
перезаписи.

## Кто её создаёт

| Действие | Выполняется в | Триггер |
| - | - | - |
| Плановая резервная копия + очистка | `worker` | `APP_BACKUP_INTERVAL_HOURS`, плюс одна сразу при старте |
| Ручная резервная копия | `bot` | `/ops` → **Сделать копию сейчас** |
| Восстановление | `bot` | `/ops` → **Восстановить последнюю…** |
| Просмотр списка | `api` | `GET /v1/admin/backups`, `GET /v1/panel/settings/backups` |

> [!NOTE]
>
> Операторская панель в отношении резервных копий работает только на чтение. Нет ни маршрута
> панели, ни маршрута `/v1/admin`, который создаёт или восстанавливает копию; единственные два
> пути записи в продукте — это расписание воркера и поверхность `/ops` у бота. Это сделано
> намеренно: восстановление разрушительно, и оно живёт за явно перечисленным списком аккаунтов
> Telegram, а не за правом доступа, которое можно выдать шести ролям.

## Включение резервных копий

```bash theme={null}
APP_BACKUP_ENABLED=true
APP_BACKUP_ENCRYPTION_KEY=$(openssl rand -base64 32)
APP_BACKUP_DIR=/var/lib/omniflow/backups
APP_BACKUP_INTERVAL_HOURS=24
APP_BACKUP_RETENTION_DAYS=14
```

| Переменная | По умолчанию | Назначение |
| - | - | - |
| `APP_BACKUP_ENABLED` | `false` | Включает плановые резервные копии в воркере и действия с копиями в боте. |
| `APP_BACKUP_ENCRYPTION_KEY` | *(нет)* | 32 байта в base64. Обязателен всегда, когда резервные копии включены; иначе процесс завершается с сообщением `APP_BACKUP_ENCRYPTION_KEY is required when APP_BACKUP_ENABLED is true`. |
| `APP_BACKUP_DIR` | `/var/lib/omniflow/backups` | Создаётся с режимом `0700`; файлы записываются с `0600`. При нескольких репликах воркера должен быть общим томом: плановая резервная копия оказывается на той реплике, которая выполнила проход. |
| `APP_BACKUP_INTERVAL_HOURS` | `24` | Интервал расписания. Ноль или неразбираемое значение откатывается к 24 часам. |
| `APP_BACKUP_RETENTION_DAYS` | `14` | Проставляется в каждой строке резервной копии как `retain_until`. |
| `APP_PG_DUMP_PATH` | `pg_dump` | Переопределите, если клиентские утилиты не в `PATH`. |
| `APP_PG_RESTORE_PATH` | `pg_restore` | То же самое, для восстановлений. |

> [!WARNING]
>
> Храните `APP_BACKUP_ENCRYPTION_KEY` там, куда сама резервная копия базы не достаёт, —
> в менеджере паролей, в хранилище секретов или в запечатанном конверте. Ключ, который
> существует только внутри того, что он расшифровывает, — не ключ.
>
> Это **другой** ключ, не `APP_DATA_ENCRYPTION_KEY`. Сохранить нужно оба: ключ резервных копий,
> чтобы прочитать дамп, и ключ данных, чтобы прочитать внутри него зашифрованные контактные
> значения, секреты TOTP и учётные данные провайдеров. Восстановление с неправильным ключом
> данных даёт базу, которая загружается, но чьи запечатанные значения не открываются.

Образы бота и воркера поставляются с соответствующими клиентскими утилитами PostgreSQL 18. В
compose-стеке оба монтируют общий том `backup-data` в `/var/lib/omniflow/backups`, поэтому
пересборка контейнера никогда не выбрасывает резервные копии.

Пароль базы данных никогда не попадает в argv дочернего процесса, где любой локальный
пользователь мог бы прочитать его из списка процессов. Вместо этого DSN раскладывается на
`PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD` и `PGSSLMODE` в окружении дочернего
процесса.

## Проверка

Резервная копия записывается как `completed` только после того, как её прочитали обратно: файл
расшифровывается от начала до конца, а его SHA-256-дайджест сравнивается с дайджестом,
посчитанным при записи. Копия, не прошедшая любую из этих проверок, удаляется и записывается
как `failed` с классифицированным кодом ошибки.

Это выполняется при каждой резервной копии, а не только по требованию. Непроверенная резервная
копия — это догадка.

| Код ошибки | Причина |
| - | - |
| `command_exit_N` | `pg_dump` или `pg_restore` завершился с ненулевым кодом |
| `verification_failed` | Файл не удалось прочитать обратно, либо его дайджест не совпал |
| `file_missing` | Каталог резервных копий недоступен для записи или не смонтирован |
| `cancelled` | Процесс завершился посреди копирования; `pg_dump` отменяется вместе со своим контекстом |
| `backup_failed` | Всё остальное, что сервис не смог классифицировать |

## Управление из Telegram

Telegram несёт только операторские уведомления и действия по резервным копиям и
восстановлению. Все остальные административные органы управления живут в веб-панели.

```bash theme={null}
APP_OPERATOR_TELEGRAM_IDS=111111111,222222222
```

Аккаунт, отсутствующий в этом списке, не получает на `/ops` **вообще никакого ответа** — даже
отказа, который подтвердил бы существование команды. Команда также игнорируется вне приватного
чата с ботом.

`/ops` показывает пять последних резервных копий с именем файла, статусом (и кодом ошибки, если
он записан), размером и меткой времени в UTC, а также:

* **💾 Сделать копию сейчас** — немедленно создаёт ручную резервную копию. Если одна уже
  выполняется, ответом будет *«A backup is already running»*, а не запуск второй.
* **♻️ Восстановить последнюю…** — предлагается только при наличии завершённой копии.
  Подтверждение в два шага: первое нажатие лишь показывает предупреждение, а второе несёт
  идентификатор той резервной копии, которую оператор видел.
* **🔄 Обновить** — перерисовывает список.

Панель показывает только размеры и метки времени: никаких параметров подключения, путей к
файлам и материала шифрования.

Оба действия пишут события журнала аудита, доступные только на добавление, с указанием
аккаунта Telegram, действия, резервной копии и причины: `backup.created` для ручной копии,
затем `backup.restore_requested` и `backup.restored` вокруг восстановления. Причина
восстановления записывается как *«restore requested from Telegram by operator \<id>»*.

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

Восстановление сначала проверяет резервную копию, затем запускает
`pg_restore --clean --if-exists --no-owner --no-privileges --single-transaction` с
расшифрованным потоком на stdin. Оно намеренно прямолинейно, и именно поэтому настаивает,
чтобы вызывающая сторона подтвердила разрушительное намерение и чтобы было записано, кто
попросил.

> [!WARNING]
>
> Восстановление заменяет текущее содержимое базы данных. Все изменения, сделанные с момента
> создания резервной копии, теряются — включая оформленные заказы, проведённые платежи и
> полученные сообщения поддержки за этот промежуток.

Оно отказывает, а не выполняется наполовину, в следующих случаях:

| Отказ | Причина |
| - | - |
| `backups are not configured` | `APP_BACKUP_ENABLED` выключен или ключ не ровно 32 байта |
| `a restore requires an operator and a reason` | Оба обязательны и оба сохраняются в строке `backup_restores` |
| `backup is not restorable` | Статус резервной копии не `completed` |
| ошибка дайджеста или кадра | Файл не прошёл повторную проверку; против базы данных ничего не запускается |

Перед восстановлением:

1. Остановите `api`, `bot` и `worker`. Живой воркер продолжит выдавать доступ поверх
   состояния, которое вот-вот будет заменено.
2. Сделайте свежую резервную копию текущей базы, даже если вы считаете её сломанной. Это
   единственный путь назад из ошибочного восстановления.
3. Убедитесь, что статус целевой резервной копии — `completed`.

После восстановления:

1. Запустите сначала `worker` и следите за `/readyz` и топиком исполнения заказов.
2. Запустите `api` и `bot`.
3. Сверьтесь с Remnawave. Выдача доступа, случившаяся после точки резервной копии, всё ещё
   живёт в панели, но отсутствует в Omniflow. Плановое обнаружение расхождений сообщит о
   несоответствиях в течение пятнадцати минут, и каждое из них решается из представления
   расхождений.
4. Перепроверьте платежи в ожидании. Платёж, проведённый после точки резервной копии,
   подхватывается опросом сверки или повторной доставкой вебхука провайдера. Оба идемпотентны,
   поэтому ничто не списывается и не зачисляется дважды.

Восстановление выполняется в процессе **bot**, когда оно запущено из `/ops`, поэтому именно в
его логе описан сбой, хотя сообщение на экране упоминает воркер.

### Восстановление без бота

Бинарник воркера заодно работает как фильтр расшифровки, потому что аварийное восстановление не
должно зависеть от способности остального стека запуститься. Ему не нужно ничего, кроме
бинарника, ключа и файла:

```bash theme={null}
# Decrypt with the key from your secret store.
APP_BACKUP_ENCRYPTION_KEY=... \
  docker compose run --rm -T worker --decrypt-backup \
  < omniflow-scheduled-20260811T030000Z.dump.enc > backup.dump

# Restore with the client tools that ship in the postgres image.
docker compose exec -T postgres pg_restore \
  --clean --if-exists --no-owner --no-privileges --single-transaction \
  --dbname "postgres://omniflow:omniflow@localhost:5432/omniflow" < backup.dump
```

`--decrypt-backup` должен быть **первым** аргументом, а ключ читается из окружения до загрузки
какой-либо конфигурации — не требуется ни база данных, ни Valkey, ни учётные данные Remnawave.
Он падает с `APP_BACKUP_ENCRYPTION_KEY is required to decrypt a backup` или
`APP_BACKUP_ENCRYPTION_KEY must be base64-encoded 32 bytes`, а при неверном ключе или
повреждённом файле падает громко, а не выдаёт частичный вывод.

> [!NOTE]
>
> Atlas записывает применённые миграции в собственную схему `atlas_schema_revisions`, а не в
> `public`. Дамп, снятый Omniflow, несёт всю базу целиком, поэтому обычное восстановление
> переносит эту историю вместе с ней. Если вы пересобираете базу иным способом, перенесите эту
> схему тоже, иначе сервис `migrate` попытается применить историю заново, а диагностический
> комплект сообщит о нуле применённых миграций.

## Срок хранения и очистка

Каждая строка резервной копии при создании помечается `retain_until` — на
`APP_BACKUP_RETENTION_DAYS` вперёд. После каждого планового запуска воркер очищает до 100
просроченных копий: файл удаляется, а строка помечается как `pruned`. Строка сохраняется,
поэтому история того, какие копии существовали, остаётся пригодной для аудита после того, как
файлы исчезли.

Файл, который не удалось удалить, логируется и пропускается, а его строка сохраняет свой
статус, поэтому следующий проход пробует снова, а не теряет его из виду.

## Просмотр истории

| Поверхность | Аутентификация | Возвращает |
| - | - | - |
| `GET /v1/panel/settings/backups?limit=` | `settings.read` | `{backups, restores}` вместе |
| `GET /v1/admin/backups?limit=` | Bearer-токен `APP_OPERATOR_TOKEN` | Только копии; limit 1–100, по умолчанию 20 |
| `/admin/settings/operations` → карточка Backups | `settings.read` | Те же два списка, отрисованные |
| `/ops` в Telegram | Перечисленные аккаунты операторов | Пять последних |

Резервная копия, которую никто никогда не восстанавливал, — это копия, о которой никто не
знает, работает ли она; именно поэтому история восстановлений находится рядом со списком
копий, а не на отдельном экране.

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

Миграции применяются только вперёд, и их накатывает сервис `migrate` до старта любого
прикладного процесса.

1. Сделайте резервную копию и убедитесь, что она `completed`.
2. Загрузите новые образы. Закрепляйте явный тег; плавающие теги делают откат неоднозначным.
3. Выполните `docker compose up -d`. Сервис `migrate` запускает `atlas migrate apply`, и все
   остальные сервисы ждут его успешного завершения.
4. Следите за `/readyz` на каждом процессе.

Повторное применение миграций ничего не делает: Atlas помнит, что уже выполнено. CI
доказывает, что каждая поддерживаемая база — пустая база плюс каждый префикс истории
миграций — обновляется до головной версии, и что повторное применение головной ничего не
меняет.

> [!NOTE]
>
> Сервис `migrate` в compose использует жёстко заданную строку подключения и не читает
> `APP_DATABASE_URL`. Если вы поменяли учётные данные базы, поменяйте их и в `compose.yaml`,
> иначе мигратор подключится не к той базе.

## Откат

Omniflow не поставляет обратных миграций. Откат схемы — это восстановление:

1. Остановите `api`, `bot` и `worker`.
2. Восстановите резервную копию, снятую перед обновлением.
3. Закрепите предыдущие теги образов и запустите стек.
4. Сверьтесь с Remnawave, как описано выше.

Именно поэтому первый шаг обновления — резервная копия, и именно поэтому копия проверяется,
прежде чем ей начинают доверять.

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

Omniflow поддерживает PostgreSQL 18. Чтобы перейти между мажорными версиями:

1. Остановите `api`, `bot` и `worker`; `postgres` оставьте работающим.
2. Сделайте резервную копию и проверьте её.
3. Остановите `postgres` и запустите новую мажорную версию поверх **пустого** каталога данных.
4. Восстановите резервную копию.
5. Запустите прикладные процессы.

Не направляйте новую мажорную версию PostgreSQL на старый каталог данных. `pg_upgrade` —
альтернатива, если вы предпочитаете обновление на месте, но путь «дамп и восстановление» — тот,
под который спроектирован этот формат резервных копий. Помните, что `pg_dump` и `pg_restore`
должны соответствовать мажорной версии сервера; поставляемые образы фиксируют клиентские
утилиты PostgreSQL 18 именно по этой причине.

## Учения по аварийному восстановлению

Проводите их ежеквартально. Непроверенная резервная копия — это предположение.

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

Скрипт — это сама процедура, а не её описание. Он берёт последнюю резервную копию со
статусом `completed`, расшифровывает её через `worker --decrypt-backup`, восстанавливает в
одноразовый экземпляр PostgreSQL в сети самого стека, сравнивает число строк, открывает
запечатанное значение ключом `APP_DATA_ENCRYPTION_KEY` и снова удаляет экземпляр. Он никогда
не пишет в боевую базу и завершается с ненулевым кодом, когда учения провалились, — поэтому
запуск по расписанию что-то значит.

Передайте `COMPOSE`, если стек использует файлы переопределения, и `TABLES`, чтобы сравнить
другой набор:

```bash theme={null}
COMPOSE="docker compose -f compose.yaml -f compose.prod.yaml" sh tools/restore-drill.sh
```

Что он сообщает и как это читать:

| Вердикт | Что значит |
| - | - |
| `same` | Восстановленная таблица совпадает с боевой. |
| `live moved on` | В боевой таблице строк больше, чем в копии. Ожидаемо для работающей установки: база продолжала работать после снятия копии. Для точного сравнения запускайте учения сразу после создания резервной копии. |
| `MORE THAN LIVE` | В копии есть строки, которых нет в боевой базе. Дрейфом это невозможно; копия сделана не с этой базы. Учения провалены. |
| `MISSING` | Таблицы в копии нет. Схема не перенеслась. Учения провалены. |

Шаг с запечатанным значением пропускают чаще всего, и он же чаще всего проваливается: база,
восстановленная без её ключа шифрования данных, поднимается безупречно, а секреты в ней
нечитаемы. Скрипт открывает запечатанный клиентский секрет customer OIDC в одноразовом
контейнере, поэтому на хосте ничего устанавливать не нужно. Если в установке не настроен ни
один OIDC-провайдер, запечатанного значения для проверки нет — скрипт скажет об этом, а не
пройдёт молча; проверьте вручную секрет TOTP или учётные данные провайдера, прежде чем
доверять такой копии.

Записывайте дату и результат.

## Больше, чем база данных: полное аварийное восстановление

Всё выше отвечает на вопрос «PostgreSQL потерян или повреждён». Катастрофа на уровне хоста —
более широкий вопрос, и ответ на большую его часть таков: специальная процедура не нужна,
потому что PostgreSQL — единственное долговременное хранилище, — но для двух вещей вне базы
план всё же нужен.

### Потеря всего хоста

Восстановление `pg_dump` возвращает каждую строку. Оно не возвращает то, что всегда жило
только вне базы данных:

* **`.env`**, либо то, что хранит его в вашем менеджере секретов.
* **`APP_DATA_ENCRYPTION_KEY`.** Это то, что важнее всего восстановить в первую очередь.
  Каждое запечатанное значение — секреты TOTP, клиентские секреты OIDC, учётные данные
  провайдеров, контактные значения клиентов — навсегда недоступно без него, и пути
  повторного шифрования не существует. См.
  [Безопасность](https://omniflow-vpn.mintlify.site/ru/operations/security.md#запечатывание-секретов-и-принцип-только-на-запись).
* **`APP_BACKUP_ENCRYPTION_KEY`**, хранимый отдельно от резервных копий, которые он
  расшифровывает, — копия, несущая оба ключа, это единственный артефакт, который
  расшифровывает сам себя.
* **`APP_OPERATOR_TOKEN`**, поскольку новый никто не выпускает; утраченный токен означает
  повторную инициализацию поверхности с bearer-токеном.
* **Конфигурация обратного прокси и материал TLS** перед стеком, которые живут в вашем
  собственном хозяйстве, а не в Omniflow. См. [Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).
* **DNS**, указывающий на то, что заменяет утраченный хост.

Ничего из этого не входит в формат резервной копии на этой странице, потому что ничто из
этого не строки в PostgreSQL. Храните это там же, где вы храните секреты, которые должны
пережить то, что они защищают, — никогда внутри самой резервной копии базы данных.

### Потеря Valkey

Это вообще не требует восстановления. Valkey хранит записи кэша, счётчики ограничения
частоты запросов, блокировки и токены однократного использования — ничего долговременного, —
поэтому утраченный экземпляр Valkey — это просто новый, запущенный пустым. См.
[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md#почему-valkey-настроен-без-персистентности).

Автоматическое определение [режима обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md#что-оно-отслеживает-на-самом-деле)
следит за Valkey наравне с PostgreSQL и Remnawave, поэтому сбой приостанавливает новые покупки
после той же серии отказов, что и любая другая зависимость, вместо того чтобы клиенты
сталкивались с противоречивыми отказами ограничения частоты запросов и защиты от повтора
посреди оформления заказа.

### RPO и RTO

Изложено на основе того, что реально настраивается, а не обещанной цифры, которую эта
страница не может гарантировать:

* **Целевая точка восстановления (RPO)** определяется `APP_BACKUP_INTERVAL_HOURS` —
  по умолчанию 24 часа между плановыми резервными копиями, поэтому в худшем случае можно
  потерять активность за этот период. Уменьшите интервал ради более узкого RPO; ручная
  резервная копия из `/ops` перед рискованной операцией сужает его до нуля на этот момент.
* **Целевое время восстановления (RTO)** определяется прежде всего временем восстановления,
  которое растёт вместе с размером базы данных, плюс окном сверки после возвращения стека —
  расхождения с Remnawave устраняются в течение пятнадцати минут после планового прохода
  сверки, а платежи в процессе рассчитываются на следующем тике сверятеля. Здесь нет
  фиксированной цифры, которую можно обещать: используйте продолжительность собственных
  учений как ориентир для вашей установки.

## Связанные страницы

> **[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md)**
>
> Топология compose, тома и путь обновления, которые предполагают эти процедуры.

> **[Диагностика неисправностей](https://omniflow-vpn.mintlify.site/ru/operations/troubleshooting.md)**
>
> Что означает каждый код ошибки резервного копирования и где читать лог.


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