Бэкап и восстановление AFFiNE
AFFiNE — гибрид заметок, канбана и вики, который многие ставят на свой VPS как замену Notion именно ради контроля над данными. Но этот контроль работает в обе стороны: если на сервере сгорит диск или вы случайно снесёте не тот контейнер, никакого «восстановить из корзины» в облаке не будет — только то, что сами вынесли за пределы сервера. У AFFiNE в self-host три независимых места хранения данных вместо одного файла, и если бэкапить только «очевидное» (базу), после восстановления заметки открываются пустыми или без картинок. Разберём, что копировать и в каком порядке восстанавливать, чтобы не наступить на эти грабли.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что и где хранит AFFiNE при self-host развёртывании
Стандартное self-host развёртывание AFFiNE — четыре контейнера: affine (сам сервер), affine_migration (разовая миграция схемы при старте), postgres (образ pgvector/pgvector:pg16 — PostgreSQL 16 со встроенным pgvector для векторного поиска) и redis. Персистентных источников данных из них — три, Redis в их число не входит.
| Компонент | Переменная в .env | Что содержит | Критичность |
|---|---|---|---|
| PostgreSQL | DB_DATA_LOCATION | Пользователи, воркспейсы, права доступа, сами документы (заметки, канбан, вики — как CRDT-снимки в таблицах) | Обязательно |
| Storage (blobs) | UPLOAD_LOCATION | Вложения, изображения, аватары, файлы, сгенерированные ИИ-блоки | Обязательно |
| Config | CONFIG_LOCATION | config.json и автоматически сгенерированный private.key | Обязательно |
| Redis | — | Кэш, очередь фоновых задач, синхронизация между вкладками | Не нужно бэкапить |
Ключевой нюанс, который отличает AFFiNE от простых self-host заметочников вроде Trilium или Joplin: сам текст документов лежит не в отдельном файле, а внутри PostgreSQL — в виде CRDT-снимков (Yjs), которые движок использует для совместного редактирования. А бинарные вложения — картинки, файлы, вставленные в заметки, — физически хранятся отдельно, на файловой системе (UPLOAD_LOCATION) либо, если подключено внешнее S3-совместимое хранилище (свой MinIO, Cloudflare R2, AWS S3), — в бакете. Из-за этого разделения бэкап только базы без storage гарантированно неполный: заметки откроются, но картинки и файлы в них будут битыми ссылками.
Redis здесь — только кэш, брокер фоновых задач и синхронизация между открытыми сессиями, ничего постоянного там не хранится, поэтому в reference-конфигурации для него даже не создают volume. Бэкапить нечего.
Дамп PostgreSQL — ядро бэкапа
Для дампа базы используйте pg_dump в custom-формате (-Fc) — он сжат, поддерживает выборочное восстановление и, в отличие от обычного .sql-дампа, восстанавливается командой pg_restore с флагами, которые сами разбираются с владельцами объектов и правами:
docker compose exec -T postgres pg_dump \
-U "$DB_USERNAME" -Fc -d "${DB_DATABASE:-affine}" \
> /backups/affine/affine_$(date +%F_%H%M).dump
Значения DB_USERNAME и DB_DATABASE берутся из вашего .env — если не помните, гляньте в него напрямую (grep DB_ .env) или в переменные окружения контейнера postgres (docker compose exec postgres env | grep POSTGRES).
Важная деталь именно для AFFiNE: база использует расширение pgvector (для векторного поиска), которое физически «зашито» в образ pgvector/pgvector:pg16, а не устанавливается вручную. Custom-формат дампа при восстановлении сам выполнит CREATE EXTENSION vector, но только если на целевом сервере поднят тот же образ с уже вкомпилированным расширением — восстановление в обычный postgres:16 без pgvector упадёт на этом шаге. Правило простое: на новом сервере поднимайте postgres строго из того же образа, что был в рабочем docker-compose.yml.
Дамп можно снимать на живой базе без остановки AFFiNE — PostgreSQL сам обеспечивает консистентный снимок за счёт MVCC, отдельная блокировка не требуется.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверBlob-хранилище: без него дамп бесполезен
Если blobs хранятся локально (дефолтный режим, без подключённого S3), вся директория UPLOAD_LOCATION — такой же обязательный кусок бэкапа, как и база. Она смонтирована в контейнер affine bind-маунтом (не именованным Docker-volume), поэтому копируется обычными средствами файловой системы:
tar czf /backups/affine/storage_$(date +%F_%H%M).tar.gz -C "$UPLOAD_LOCATION" .
Если на одном VPS несколько сервисов и вы уже сталкивались с проблемами прав доступа при копировании смонтированных директорий — этому посвящён разбор бэкапа Docker-volume и типичных ошибок: грабли с UID контейнера и битыми правами после восстановления не специфичны для AFFiNE, но регулярно всплывают именно на этом шаге.
Если вместо локального диска подключено внешнее S3-совместимое хранилище (свой MinIO, R2 или AWS S3) — блобы физически лежат в бакете, а не в UPLOAD_LOCATION. Резервное копирование блобов тогда — задача самого S3-хранилища (версионирование бакета, репликация), а не скрипта на AFFiNE-сервере; проверьте это отдельно, если план бэкапов построен на копировании локальной директории — при S3-режиме она может быть почти пустой, и это нормально.
config.json, private.key и .env — то, что забывают чаще всего
Директория CONFIG_LOCATION невелика по объёму, но в ней два файла, без которых восстановленный сервер не заработает как надо:
config.json— настройки сервера: внешний URL (server.externalUrlилиAFFINE_SERVER_EXTERNAL_URL), почтовый релей для писем-приглашений, параметры хранилища и индексатора;private.key— секретный ключ, который AFFiNE генерирует автоматически при первом деплое (на шагеaffine_migration) и использует для подписи и шифрования части данных. Его нельзя «сгенерировать заново» без последствий — замена ключа делает недоступными данные, которые были подписаны или зашифрованы старым.
Практическое правило: копируйте всю директорию CONFIG_LOCATION целиком, а не только config.json, и никогда не давайте свежему деплою AFFiNE сгенерировать новый private.key поверх старого при восстановлении — сначала подложите файл из бэкапа, потом запускайте контейнеры.
cp -r "$CONFIG_LOCATION" /backups/affine/config_$(date +%F_%H%M)
Отдельно — сам .env и docker-compose.yml. В .env — не только пароль к базе (DB_PASSWORD), но и версия образа (AFFINE_REVISION), от которой зависит, какая схема миграций уже применена. Восстановление под другой версией может запустить миграции, не совпадающие с тем, что было на момент бэкапа — поэтому версию образа стоит фиксировать конкретным тегом, а не плавающим stable, и хранить рядом с остальными файлами бэкапа.
Автоматический скрипт бэкапа и вынос за пределы сервера
Собираем всё в один скрипт и кладём в cron. Пути ниже — переменные из вашего .env, поправьте под себя, если раскладка отличается от ~/.affine/self-host/{postgres,storage,config} из официальных примеров:
#!/bin/bash
set -euo pipefail
source /opt/affine/.env # DB_USERNAME, DB_DATABASE, UPLOAD_LOCATION, CONFIG_LOCATION
DEST="/backups/affine/$(date +%F_%H%M)"
mkdir -p "$DEST"
# 1. Дамп PostgreSQL в custom-формате
docker compose -f /opt/affine/docker-compose.yml exec -T postgres pg_dump \
-U "$DB_USERNAME" -Fc -d "${DB_DATABASE:-affine}" \
> "$DEST/affine.dump"
# 2. Blob-хранилище (пропустите, если используете внешний S3)
tar czf "$DEST/storage.tar.gz" -C "$UPLOAD_LOCATION" .
# 3. Конфиг вместе с private.key
cp -r "$CONFIG_LOCATION" "$DEST/config"
# 4. .env и docker-compose.yml — чтобы знать, из чего разворачивать
cp /opt/affine/.env /opt/affine/docker-compose.yml "$DEST/"
# 5. Ротация — храним 14 последних снимков
find /backups/affine -maxdepth 1 -mtime +14 -type d -exec rm -rf {} \;
echo "Backup done: $DEST"
В cron:
0 4 * * * /opt/scripts/affine-backup.sh >> /var/log/affine-backup.log 2>&1
Локальная папка /backups/affine на том же VPS — гигиенический минимум, а не полноценная защита: она не переживёт отказ диска или компрометацию сервера целиком. Синкайте результат на отдельное хранилище — для смешанных бэкапов (дамп + архив + конфиги в одной папке) удобен restic с его дедупликацией и шифрованной историей версий:
restic -r /mnt/backup-storage/affine-repo backup /backups/affine/$(date +%F)*
Если restic ещё не настроен, есть пошаговая установка restic на VPS и готовый docker-compose с restic, если удобнее держать его в контейнере. А если под бэкапы вообще ещё нет отдельного сервера — вот как выбрать и настроить VPS под бэкапы и архив: держать копии физически отдельно от рабочего сервера с AFFiNE — стандартная практика 3-2-1, особенно когда в базе — рабочие документы, которые больше негде взять.
Как грубый ориентир по объёму (по данным самого AFFiNE, у вас будет иначе в зависимости от вложений): около 100 МБ роста PostgreSQL на каждую 1000 документов при среднем документе в 1000 слов — не точная цифра, но достаточная, чтобы прикинуть порядок величины диска под бэкапы.
Восстановление AFFiNE на новом сервере
Порядок восстановления важен — блобы и конфиг должны оказаться на месте раньше первого полноценного запуска сервера, а образ базы данных обязан совпадать:
1. Разверните docker-compose.yml и .env из бэкапа, зафиксировав тот же AFFINE_REVISION и тот же образ Postgres (pgvector/pgvector:pg16 или та версия, что была в работе) — не запускайте docker compose up -d сразу, сначала поднимите только postgres и redis.
2. Восстановите базу в custom-формате:
docker compose up -d postgres redis
docker compose exec -T postgres pg_restore \
--clean --if-exists --no-owner --no-privileges \
-U "$DB_USERNAME" -d "${DB_DATABASE:-affine}" \
< /backups/affine/2026-08-30_0400/affine.dump
Флаги --clean --if-exists сносят объекты перед восстановлением (на случай, если affine_migration уже создала пустую схему при первом старте), а --no-owner --no-privileges избавляют от ошибок несовпадения ролей между старым и новым сервером.
3. Подложите storage и config до старта самого AFFiNE:
mkdir -p "$UPLOAD_LOCATION" "$CONFIG_LOCATION"
tar xzf /backups/affine/2026-08-30_0400/storage.tar.gz -C "$UPLOAD_LOCATION"
cp -r /backups/affine/2026-08-30_0400/config/. "$CONFIG_LOCATION"
4. Запустите оставшиеся контейнеры и проверьте логи:
docker compose up -d
docker compose logs -f affine affine_migration
5. Проверьте в браузере, а не только в логах: зайдите под существующим пользователем, откройте воркспейс, раскройте документ со вложенной картинкой — это единственный надёжный способ убедиться, что восстановились не только записи в базе, но и связанные с ними блобы. В обсуждениях сообщества AFFiNE не раз всплывал ровно этот сценарий: человек восстанавливает только дамп PostgreSQL, контейнеры стартуют без ошибок, но заметки не появляются или появляются без вложений — потому что storage и config на новом сервере пустые или пересозданы с новым private.key. Три компонента бэкапа не заменяют друг друга.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Нужно ли останавливать AFFiNE перед снятием дампа PostgreSQL?
Нет, pg_dump берёт консистентный снимок на живой базе за счёт MVCC. Останавливать контейнеры стоит только перед восстановлением, чтобы affine_migration не успела создать пустую схему поверх той, что вы восстанавливаете.
Обязательно ли бэкапить Redis?
Нет. Он используется только как кэш, очередь фоновых задач и синхронизация — постоянных данных там не хранится, и в reference-конфигурации для него даже не создают volume.
Что будет, если восстановить только базу без storage и config?
Учётные записи и текст документов на месте, но вложения превратятся в битые ссылки, а без исходного private.key часть подписанных/зашифрованных данных может оказаться недоступной. Это самая частая причина жалоб «восстановил бэкап, а заметки пустые».
Обязательно ли восстанавливать в тот же образ PostgreSQL?
Да, если в дампе использовалось pgvector (а оно используется по умолчанию, образ pgvector/pgvector:pgXX) — восстановление в обычный postgres без этого расширения упадёт на CREATE EXTENSION vector.
Как быть, если блобы лежат не на диске, а в S3-совместимом хранилище?
Локальная UPLOAD_LOCATION тогда не отражает реальные данные, и её бэкап бессмысленен — резервное копирование блобов обеспечивает само S3-хранилище (версионирование бакета, репликация), а не скрипт на AFFiNE-сервере.
Как часто снимать бэкап?
Зависит от интенсивности работы. Для рабочего инструмента с ежедневными правками разумный минимум — раз в сутки, плюс отдельный ручной снимок перед обновлением на новую мажорную версию: миграции схемы необратимы, и откатиться проще из бэкапа «до», чем разбираться постфактум.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →