---
title: "Бэкап и восстановление"
description: "Онлайн-бэкап встроенной командой образа: снять проверенный ZIP без остановки сервиса, проверить его и восстановить в чистый корень данных."
---

# Бэкап и восстановление

Бэкап Notarium — это **встроенная команда образа**, а не копирование файлов снаружи. `notarium backup` собирает логический ZIP, пока сервис продолжает работать: чтение остаётся доступным, запись притормаживается только на две короткие контрольные точки. Нужен только Docker и работающий контейнер — останавливать сервис не придётся.

> [!danger] Не копируйте живой `meta.db`
> `cp /data/meta.db` — не бэкап. Служебная БД работает в режиме WAL: закоммиченные строки могут лежать ещё в `meta.db-wal`, а файлы, скопированные по отдельности, не являются срезом на один момент времени. Восстановленный из такой копии инстанс теряет данные молча.

## Снять бэкап

```bash
docker compose exec -T notarium backup > notarium-20260731.zip
```

Это полноценный бэкап, а не проверка «работает ли»: команда собирает архив, проверяет его собственным верификатором и только потом отдаёт байты. Флаг `-T` **обязателен**: иначе Compose выделит псевдотерминал, а через него бинарный поток не проходит без искажений — архив получится битым. У голого `docker exec` псевдотерминал по умолчанию не выделяется, поэтому там флаг не нужен. Стандартный поток вывода зарезервирован строго под байты ZIP; прогресс и итоговая сводка идут в stderr и архив не портят.

## Для регулярной джобы

У перенаправления `>` есть одна ловушка: файл под финальным именем создаётся **до** того, как команда отработает. Если Docker или бэкап упадут на середине, в каталоге останется файл с правильным именем и неполным содержимым. Ниже — публикация, безопасная при отказе: пишем во временный файл, синхронизируем на диск и публикуем атомарной жёсткой ссылкой.

```bash
backup="notarium-$(date -u +%Y%m%dT%H%M%SZ).zip"
partial="${backup}.partial.$$"
set -eu
umask 077
committed=0
cleanup() { test "$committed" -eq 1 || rm -f "$partial"; }
trap cleanup EXIT
docker compose exec -T notarium backup > "$partial"
sync -f "$partial"
sync -f "$(dirname "$backup")"
committed=1
ln "$partial" "$backup"
if sync -f "$(dirname "$backup")"; then
  if rm "$partial"; then
    sync -f "$(dirname "$backup")" ||
      echo "backup warning: final is durable; partial cleanup fsync failed" >&2
  else
    echo "backup warning: final is durable; retaining recovery partial $partial" >&2
  fi
else
  echo "backup warning: final is visible; retaining durable recovery partial $partial" >&2
fi
trap - EXIT
```

`set -e` не даёт опубликовать архив, если Docker или бэкап вернули ошибку. Временное имя содержит PID, поэтому две одновременные джобы не подерутся за один файл. Временный файл и его каталог сбрасываются на диск **до** точки публикации, а сама публикация — атомарная жёсткая ссылка без перезаписи: две джобы с одинаковым целевым именем не затрут друг друга.

Сбои уже **после** точки публикации — предупреждения, а не ложные отказы: финальный файл на месте, и в сомнительных случаях временный файл сохраняется как запасная копия. Даже неоднозначный ненулевой код от `ln` оставляет её: ссылка могла быть создана до того, как процесс прервали. `umask 077` делает архив читаемым только владельцу. Держите временный и финальный файл на одной файловой системе.

Для контейнера, поднятого голым `docker run` под именем `notarium`, всё то же самое — меняется одна строка:

```bash
docker exec notarium backup > "$partial"
```

> [!tip] Если каталог для бэкапов контейнеру уже виден
> Обвязка выше нужна потому, что stdout-транспорт не может опубликовать файл сам. Когда каталог под архивы уже смонтирован — backup-шара, NFS, scratch-том при read-only корне, — ту же работу делает `backup --output /путь/архив.zip`: пишет во временный файл рядом с целевым, сбрасывает на диск, верифицирует и публикует атомарной жёсткой ссылкой без перезаписи, а при ошибке не оставляет под целевым именем ничего. Тогда shell-обвязка не нужна, а в stdout уходит одна JSON-сводка.

> [!note] Бэкапу нужен работающий контейнер
> Команду запускают через `docker exec` в контейнер, где уже крутится сервер, — не отдельным контейнером: чтобы снять консистентный срез, ей нужно договориться с живым приложением.

## Когда бэкап может не получиться

Бэкап устроен так, что **неконсистентного архива вы не получите молча**: если срез не удалось снять, команда завершается ошибкой и ничего не публикует. Два случая, когда это происходит:

- **Непрерывный поток правок.** Пока собирается архив, данные должны постоять на месте; пересекающаяся запись вызывает повтор, а при бесконечном потоке правок команда сдаётся с ошибкой. На практике встречается на активном инстансе — просто повторите позже.
- **Идёт длинный импорт или экспорт.** Бэкапу нужна пара очень коротких пауз в записи, и длинная задача в них не укладывается. Не ставьте бэкап на то же окно, что и массовый импорт.

В обоих случаях сервис не страдает: очередь записи освобождается немедленно, операторская команда не держит приложение. Обычное чтение доступно всё время работы бэкапа в любом случае.

## Что внутри архива

| Путь в архиве | Что это |
|---|---|
| `data/meta.db` | Учётки, сессии, членство, стабильные идентификаторы, история версий и состояние задач. |
| `data/spaces/` | Markdown-истина, включая память агента и файлы-маркеры проектов. |
| `data/jobs/` | Готовые артефакты и durable-загрузки импорта. |
| `manifest.json` | Версия формата, отметка времени, точный набор каталогов, размеры, mtime и SHA-256 каждого файла. |

Производный каталог `data/engine/` в архив **не** входит: индексы пересобираются из файлов после восстановления. Из незавершённых файлов пропускаются только служебные — временные файлы атомарной записи заметок, недокачанные загрузки импорта и части артефактов экспорта. Обычные пользовательские файлы, чьё имя оканчивается на `.part`, остаются в архиве: они легитимны.

> [!warning] Архив — чувствительные данные
> ZIP содержит учётные записи и состояние сессий из служебной БД. Храните его как секрет: `umask 077` в сниппете выше делает новый архив читаемым только владельцу.

## Проверка

Проверка ничего не меняет и должна быть частью каждой бэкап-джобы:

```bash
docker compose exec -T notarium backup verify < notarium-20260722.zip

# для голого docker run:
docker exec -i notarium backup verify < notarium-20260722.zip
```

При успехе команда печатает одну JSON-сводку и выходит с нулевым кодом. Отклоняются: небезопасные и дублирующиеся пути, неучтённые в манифесте файлы, неточный набор каталогов, расхождения размеров и хешей, некорректные метаданные времени, превышение лимитов и непройденная проверка целостности SQLite. Живой каталог данных при этом не читается и не меняется.

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

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

Восстановление — **офлайн-операция аварии**. Оно принимает только чистый пустой корень данных и никогда не сливается с существующим инстансом и не перезаписывает его.

Подготовьте свежий том и переключите на него сервис **до** запуска:

```bash
set -eu
docker compose stop notarium
# отведите старый том в сторону; подключите в compose пустой /data
docker compose run --rm --no-deps -T notarium restore \
  < notarium-20260722.zip
docker compose up -d --force-recreate --no-deps notarium
```

Контейнер после этого нужно именно пересоздать: `docker compose start` поднял бы старый контейнер со старой конфигурацией монтирования. Старый том держите, пока не проверите восстановленный инстанс.

Восстановление проверяет весь архив целиком до установки. Если процесс прервали посреди установки, остаётся явный маркер: такую цель считайте одноразовой и восстанавливайтесь в новую пустую, а не «дожимайте» и не сливайте.

Что проверить после восстановления:

1. Войти учётной записью из бэкапа.
2. Открыть несколько пространств и убедиться, что адреса и идентификаторы на месте.
3. Открыть правленую заметку и посмотреть её историю.
4. Проверить задачи импорта и экспорта, чьи загрузки или артефакты вам важны.

> [!note] Совместимость со схемой служебной БД
> Восстанавливаемая служебная БД должна нести реестр миграций, который принимает целевая сборка. Непустая БД без реестра падает закрыто — восстановление не угадывает её версию и не проставляет отметку само. См. [База данных](/docs/self-hosting/database/).

## Границы применимости

Встроенная команда поддерживает только каноничную раскладку: единый корень данных и служебная БД в файле SQLite. Если `META_DB_URL` указывает на Postgres или заметки лежат вне `DATA_DIR`, команда **падает закрыто**, а не отдаёт частичный архив: в этом случае пользуйтесь штатными средствами вашей БД и снимками смонтированных каталогов.

Промежуточные файлы бэкапа и проверки по умолчанию живут в `/tmp`. Потоковый бэкап проверяет сам себя до публикации, и ему может временно понадобиться место под архив плюс два развёрнутых стейджа; отдельной проверке — архив плюс один. Восстановление буферизует в scratch входящий поток, а разворачивает архив уже в сам свежий корень данных. Если корень контейнера смонтирован только на чтение или данных много, укажите `NOTARIUM_BACKUP_TMPDIR` на смонтированный каталог с записью.

Сжатый и развёрнутый вход ограничены 64 ГиБ и одним миллионом записей; имена, служебные структуры ZIP и `manifest.json` имеют отдельный лимит памяти в 32 МиБ. Доверенные крупные инсталляции могут поднять `NOTARIUM_BACKUP_MAX_BYTES`, `NOTARIUM_BACKUP_MAX_ENTRIES` и `NOTARIUM_BACKUP_MAX_METADATA_BYTES` — см. [Переменные окружения](/docs/reference/environment-variables/).

## Дальше

- [CLI образа](/docs/self-hosting/cli/) — полный контракт команд, потоков и кодов возврата.
- [База данных](/docs/self-hosting/database/) — что именно хранит служебная БД и почему её нельзя не бэкапить.
- [Продакшн](/docs/self-hosting/production/) — reverse proxy, инвариант «один инстанс», эксплуатация.
