Свой сервер синхронизации Obsidian: LiveSync, CouchDB и ИИ через MCP - со всеми граблями
Заметки я держу в Obsidian на ПК и на iPhone, и отдавать их в чужое облако не хотел. Поднял свой сервер: CouchDB за nginx с TLS, плагин Self-hosted LiveSync на устройствах и рядом MCP-сервер, через который ИИ-ассистент читает и пишет те же заметки. Работает, но завелось не с первого раза: большая часть текста ниже про грабли, которых в готовых инструкциях нет.
Проверено на Obsidian 1.13.7 (Windows и iOS 18), плагине Self-hosted LiveSync 1.0.30, CouchDB 3.5.2.1, obsidian-sync-mcp v0.6.2, nginx 1.26, Debian, Docker из репозитория дистрибутива (docker.io), compose v5. Домен в примерах obsidian.example.com, хранилище "Моё хранилище", паролей нигде нет.
Что получается
ПК (Obsidian) ─────┐
iPhone (Obsidian) ─┼─HTTPS─▶ nginx :443 obsidian.example.com ─▶ 127.0.0.1:5984 CouchDB
│ ▲
Claude Code ───────┴──────────────────────▶ 127.0.0.1:8787 MCP ────────┘
(на сервере) (наружу не публикуется)
CouchDB хранит заметки, которые туда складывает плагин Self-hosted LiveSync, причём в зашифрованном виде. Наружу база смотрит только через nginx с TLS. MCP-сервер obsidian-sync-mcp читает ту же базу, расшифровывает и отдаёт заметки ИИ, слушает он только loopback. На nginx TLS и фильтр служебных адресов CouchDB.
Частый вопрос: а можно создать хранилище прямо на сервере, чтобы оно всегда было доступно? По сути так и есть, сервер хранит полную копию и работает круглосуточно. Но "создать хранилище на сервере" нельзя: Obsidian локальная программа, хранилище для него всегда папка на устройстве, веб-версии нет. Плюс заметки шифрует плагин на устройстве, сервер ключа не использует.
Поэтому кто-то один делает первую загрузку, дальше ПК можно выключать: телефон и ИИ берут всё с сервера. Каждое устройство держит полную копию, так что Obsidian работает и без интернета.
Почему не готовые скрипты в две команды
Я нашёл инструкцию вида wget ... && sudo bash script.sh. Скрипты скачал и прочитал. Запускать их было нельзя.
Первый скрипт, про безопасность сервера, делает ufw --force reset и сносит все правила файрвола, меняет порт SSH и перезапускает sshd, отчего легко потерять доступ, включает PasswordAuthentication yes, перезаписывает /etc/fail2ban/jail.local, так что остальные jail'ы исчезают, и ставит Docker из Ubuntu-репозитория через lsb_release -cs: на Debian это ломает apt update, а docker-ce конфликтует с docker.io.
Второй скрипт, сам Obsidian-стек, открывает порты 5984 и 8787 в интернет по голому HTTP, выдаёт клиенту Obsidian админские учётные данные CouchDB, не создаёт том для данных MCP, из-за чего индекс и OAuth-клиенты теряются при рестарте, использует тег :latest и называет MCP_AUTH_TOKEN токеном, хотя это пароль для формы OAuth.
Сам проект obsidian-sync-mcp хороший: OAuth 2.1 с PKCE, ротация refresh-токенов, защита от DNS-rebinding, timing-safe сравнение паролей. Плохи были только обёртки. Если инструкция предлагает запустить скрипт от root и не показывает, что внутри, открой и посмотри. Обычно там ничего страшного. Иногда там ufw --force reset.
Сервер
Что нужно заранее
Сервер с Debian или Ubuntu и Docker, nginx, поддомен с A-записью на сервер, certbot и свободные на loopback порты 5984 и 8787:
ss -tlnp | grep -E ':(5984|8787)\b' || echo "оба свободны"
Памяти связка ест около 400-600 МБ.
Плагин docker compose, не ломая системный Docker
Если Docker стоит из пакета docker.io, команды docker compose нет. Ставить docker-compose-plugin из репозитория Docker нельзя: он тянет docker-ce, который конфликтует с docker.io, и apt может снести работающий Docker. Правильно положить один бинарник:
VER=$(curl -s https://api.github.com/repos/docker/compose/releases/latest \
| grep -oP '"tag_name":\s*"\K[^"]+')
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL "https://github.com/docker/compose/releases/download/${VER}/docker-compose-linux-$(uname -m)" \
-o /usr/local/lib/docker/cli-plugins/docker-compose
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
docker compose version
Каталог и секреты
mkdir -p /opt/obsidian-sync-mcp && chmod 750 /opt/obsidian-sync-mcp
cd /opt/obsidian-sync-mcp
gen() { tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c "$1"; }
{
echo "# Obsidian Sync MCP"
echo "COUCHDB_USER=admin"
echo "COUCHDB_PASSWORD=$(gen 32)"
echo "COUCHDB_DATABASE=obsidian"
echo "LIVESYNC_USER=livesync"
echo "LIVESYNC_PASSWORD=$(gen 32)"
echo "COUCHDB_PASSPHRASE=$(gen 40)"
echo 'VAULT_NAME="Моё хранилище"'
} > .env
chmod 600 .env
| Переменная | Кому нужна |
|---|---|
COUCHDB_USER / COUCHDB_PASSWORD |
администратор базы, знает только сервер и MCP |
LIVESYNC_USER / LIVESYNC_PASSWORD |
ограниченный пользователь, вводится в Obsidian на устройствах |
COUCHDB_PASSPHRASE |
ключ E2E-шифрования, одинаковый на всех устройствах и в MCP |
VAULT_NAME |
имя хранилища для ссылок obsidian://, должно точно совпадать с именем в Obsidian |
Пароли только из букв и цифр: спецсимволы ничего не добавляют к длине 32, зато ломают экранирование в URL и в .env. Конструкция tr ... | head -c под set -o pipefail валит скрипт: tr получает SIGPIPE, код 141. А VAULT_NAME с пробелами обязательно в кавычках, иначе set -a; . ./.env в bash ломается на пробеле, docker compose кавычки читает правильно.
Конфиг CouchDB
Монтируется каталог, а не одиночный файл, и на запись (почему, смотри в граблях ниже):
mkdir -p /opt/obsidian-sync-mcp/couchdb-local.d
Файл couchdb-local.d/livesync.ini:
[couchdb]
single_node = true
max_document_size = 50000000
[chttpd]
require_valid_user = true
max_http_request_size = 4294967296
enable_cors = true
bind_address = 0.0.0.0
[chttpd_auth]
require_valid_user = true
[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true
bind_address = 0.0.0.0
[cors]
; Origin'ы клиентов Obsidian: десктоп и мобильный (capacitor).
origins = app://obsidian.md,capacitor://localhost,http://localhost
credentials = true
methods = GET, PUT, POST, HEAD, DELETE
headers = accept, authorization, content-type, origin, referer, cache-control
max_age = 3600
Адрес bind_address = 0.0.0.0 действует внутри контейнера, наружу его ограничивает compose. Строка capacitor://localhost обязательна для iOS и Android, без неё мобильный Obsidian не подключится. Проверить CORS так, как это делает iPhone:
curl -s -o /dev/null -D - -X OPTIONS -H 'Origin: capacitor://localhost' \
-H 'Access-Control-Request-Method: GET' https://obsidian.example.com/obsidian \
| grep -i access-control-allow-origin
docker-compose.yml
name: obsidian-sync-mcp
services:
couchdb:
image: couchdb:3.5.2.1
container_name: obsidian-couchdb
restart: unless-stopped
ports:
- "127.0.0.1:5984:5984"
environment:
COUCHDB_USER: ${COUCHDB_USER}
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD:?COUCHDB_PASSWORD не задан в .env}
volumes:
- couchdb_data:/opt/couchdb/data
- ./couchdb-local.d:/opt/couchdb/etc/local.d
healthcheck:
test: ["CMD", "curl", "-fsS", "-u", "${COUCHDB_USER}:${COUCHDB_PASSWORD}", "http://127.0.0.1:5984/_up"]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
mcp:
image: ghcr.io/es617/obsidian-sync-mcp:v0.6.2
container_name: obsidian-mcp
restart: unless-stopped
ports:
- "127.0.0.1:8787:8787"
environment:
COUCHDB_URL: http://couchdb:5984
COUCHDB_USER: ${COUCHDB_USER}
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
COUCHDB_DATABASE: ${COUCHDB_DATABASE}
COUCHDB_PASSPHRASE: ${COUCHDB_PASSPHRASE}
VAULT_NAME: ${VAULT_NAME}
BASE_URL: http://127.0.0.1:8787
DATA_DIR: /data
volumes:
- mcp_data:/data
depends_on:
couchdb:
condition: service_healthy
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
volumes:
couchdb_data:
mcp_data:
Три принципиальных момента: 127.0.0.1: перед каждым портом, закреплённые версии вместо :latest, том mcp_data вместе с DATA_DIR, иначе индекс и OAuth-клиенты теряются при рестарте.
docker compose config --quiet && echo "конфиг валиден"
docker compose up -d && docker compose ps
База и ограниченный пользователь
Клиентам Obsidian админа не давать: логин лежит на телефоне и ноутбуке, это самое вероятное место утечки.
cd /opt/obsidian-sync-mcp && set -a; . ./.env; set +a
A="http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@127.0.0.1:5984"
for db in _users _replicator "$COUCHDB_DATABASE"; do
curl -s -o /dev/null -w "$db → %{http_code}\n" -X PUT "$A/$db" # 412 = уже есть
done
curl -s -X PUT "$A/_users/org.couchdb.user:${LIVESYNC_USER}" -H 'Content-Type: application/json' \
-d "{\"name\":\"${LIVESYNC_USER}\",\"password\":\"${LIVESYNC_PASSWORD}\",\"roles\":[],\"type\":\"user\"}"
curl -s -X PUT "$A/${COUCHDB_DATABASE}/_security" -H 'Content-Type: application/json' \
-d "{\"admins\":{\"names\":[],\"roles\":[\"_admin\"]},\"members\":{\"names\":[\"${LIVESYNC_USER}\"],\"roles\":[]}}"
Права livesync проверяются так: своя база даёт 200, _all_dbs даёт 401 или 403, создание базы тоже 401 или 403, аноним получает 401.
Запомнить. Документ
_securityхранится внутри базы. Если базу удалить и создать заново, а плагин именно так делает при первой загрузке, права сбросятся к "только админы". Сохрани_securityзаранее и верни потом.
Переменная LIVESYNC_USER в docker-compose.yml проекта не обрабатывается, она живёт только в entrypoint для Fly.io, поэтому пользователя создаём руками.
nginx и TLS
Порядок важен: сначала HTTP-vhost, reload, потом certbot. Если в nginx есть default_server с return 444, запрос к новому домену обрывается, и ACME-проверка падает с загадочным "HTTP 000".
Сначала временный vhost, в котором открыт только /.well-known/acme-challenge/ из /var/www/html, остальное отдаёт 404. Проверка:
echo acme-ok > /var/www/html/.well-known/acme-challenge/probe
curl -s http://obsidian.example.com/.well-known/acme-challenge/probe
Сертификат через --webroot, чтобы certbot не правил конфиги nginx:
certbot certonly --webroot -w /var/www/html -d obsidian.example.com \
--non-interactive --agree-tos --keep-until-expiring
Боевой vhost:
server {
listen 80;
listen [::]:80;
server_name obsidian.example.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name obsidian.example.com;
ssl_certificate /etc/letsencrypt/live/obsidian.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/obsidian.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000" always;
# LiveSync шлёт документы пачками; лимит документа CouchDB 50 МБ.
client_max_body_size 128M;
access_log /var/log/nginx/obsidian.access.log;
error_log /var/log/nginx/obsidian.error.log;
# Админка и служебные адреса наружу не нужны.
location /_utils { deny all; }
location = /_all_dbs { deny all; }
location /_users { deny all; }
location /_node { deny all; }
location /_cluster_setup { deny all; }
location / {
proxy_pass http://127.0.0.1:5984;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# /_changes висит долгоживущими запросами.
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
proxy_request_buffering off;
}
}
У строки location /_node { deny all; } есть важное следствие: проверка сервера в мастере плагина читает /_node/_local/_config и всегда будет падать. Это нормально, подробности ниже. Отдельные логи для сайта удобны, по ним потом и шла вся диагностика клиентов.
Дальше nginx -t && systemctl reload nginx каждый раз. Если конфиг сломан, работающий nginx держит старую конфигурацию, но при следующем рестарте не поднимется и утянет все сайты на сервере.
Deploy-хук /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh, чтобы nginx перечитывал продлённый сертификат:
#!/bin/sh
set -e
if ! nginx -t 2>/dev/null; then
echo "reload-nginx: конфиг не проходит проверку, reload пропущен" >&2
exit 0
fi
systemctl reload nginx
И потом certbot renew --dry-run.
Проверки сервера
HTTPS с логином livesync даёт 200, без пароля 401, /_utils/ 403, HTTP редиректит 301. Порты 5984 и 8787 снаружи закрыты:
for p in 5984 8787; do timeout 6 bash -c "</dev/tcp/ВНЕШНИЙ_IP/$p" 2>/dev/null \
&& echo "$p ОТКРЫТ" || echo "$p закрыт"; done
MCP отвечает на initialize, а с подменённым Host: evil.example.com даёт 403. И в базе нет открытого текста заметок.
Грабли сервера
CouchDB молча падает, если ini смонтирован в режиме :ro. Контейнер перезапускается, код 1, docker logs пустой. Entrypoint делает chown по /opt/couchdb под set -e, а на read-only он не проходит.
CouchDB пишет в свой конфиг: дописывает uuid, secret и создаёт docker.ini с хешем пароля админа. Каталог конфигов содержит секреты, в git его нельзя. Если монтировать одиночный файл, хеш пропишется прямо в него.
Docker публикует порты мимо UFW. Если iptables -S DOCKER-USER показывает только -j RETURN, то ports: "5984:5984" открывает базу всему интернету, а ufw status будет показывать "закрыто". Отсюда 127.0.0.1: в каждой публикации.
Без тома DATA_DIR у MCP при каждом up теряются индекс и OAuth-регистрации, ИИ-клиент получает "Unknown client". И ещё: MCP_AUTH_TOKEN это пароль, а не bearer-токен, он вводится один раз в браузере в форме OAuth.
Бэкап и проверка восстановления
Всё это сделано до подключения устройств.
Скрипт /usr/local/bin/obsidian-backup.sh с правами 700:
#!/bin/bash
set -euo pipefail
umask 077
cd /opt/obsidian-sync-mcp
set -a; . ./.env; set +a
DEST=/var/backups/obsidian
DAY=$(date +%F)
mkdir -p "$DEST"; chmod 700 "$DEST"
# Логин передаём curl через stdin (-K -), чтобы он не светился в списке процессов.
couch() {
printf 'user = "%s:%s"\n' "$COUCHDB_USER" "$COUCHDB_PASSWORD" \
| docker exec -i obsidian-couchdb curl -fsS -K - "http://127.0.0.1:5984/$1"
}
fail() { logger -t obsidian-backup -p user.err "FAILED: $*"; echo "obsidian-backup: $*" >&2; exit 1; }
trap 'fail "line $LINENO"' ERR
TMP=$(mktemp "$DEST/.tmp-XXXXXX")
trap 'rm -f "$TMP"' EXIT
couch "${COUCHDB_DATABASE}/_all_docs?include_docs=true" | gzip > "$TMP"
ROWS=$(zcat "$TMP" | jq -e '.rows | length') || fail "dump is not valid JSON"
[ "$ROWS" -gt 0 ] || fail "dump has no rows"
mv "$TMP" "$DEST/obsidian-$DAY.json.gz"
# Права базы и пользователь livesync: без них восстановленная база не примет клиентов.
couch "${COUCHDB_DATABASE}/_security" > "$DEST/security-$DAY.json"
couch "_users/org.couchdb.user:${LIVESYNC_USER}" > "$DEST/livesync-user-$DAY.json"
find "$DEST" -maxdepth 1 -type f \( -name 'obsidian-*.json.gz' -o -name 'security-*.json' \
-o -name 'livesync-user-*.json' \) -mtime +30 -delete
logger -t obsidian-backup "OK: $ROWS docs -> $DEST/obsidian-$DAY.json.gz"
Чем он отличается от наивного curl | gzip. Связка curl -fsS и pipefail роняет скрипт при ошибке CouchDB, вместо того чтобы сохранить пустой файл. Временный файл и проверка rows > 0 не дают битому дампу затереть последний хороший. Рядом сохраняются _security и пользователь livesync с хешем, а не паролем, и это пригодилось уже через час, когда база пересоздалась. Логин уезжает в curl -K -, а не в URL, поэтому его не видно в ps. Результат идёт в syslog, читается через journalctl -t obsidian-backup.
Cron /etc/cron.d/obsidian-backup гоняет скрипт раз в сутки ночью от root. Внеплановый бэкап это просто запуск скрипта, и делать его стоит перед каждым рискованным действием.
Бэкап, который ни разу не разворачивали, это не бэкап. Восстанавливаем в тестовую базу:
set -a; . ./.env; set +a
c() { printf 'user = "%s:%s"\n' "$COUCHDB_USER" "$COUCHDB_PASSWORD" \
| docker exec -i obsidian-couchdb curl -sS -K - "$@"; }
T=obsidian_restore_test; U=http://127.0.0.1:5984
c -X PUT "$U/$T"
zcat /var/backups/obsidian/obsidian-YYYY-MM-DD.json.gz \
| jq -c '{docs: [.rows[].doc], new_edits: false}' \
| docker exec -i obsidian-couchdb sh -c 'cat > /tmp/restore.json'
c -X POST -H 'Content-Type: application/json' --data-binary @/tmp/restore.json "$U/$T/_bulk_docs"
docker exec obsidian-couchdb rm -f /tmp/restore.json
c "$U/$T" | jq .doc_count # должен совпасть с боевой базой
c -X DELETE "$U/$T"
Мелочи по ходу: _bulk_docs с new_edits:false при успехе отвечает []; stdin у curl занят конфигом -K -, поэтому тело кладём файлом внутрь контейнера; _all_docs не отдаёт _local-документы, это нормально. Полная проверка будет после первого устройства, когда станет видно, что заметки расшифровываются той же passphrase.
Главное про бэкап: дамп зашифрован passphrase. Passphrase хранить отдельно от сервера, в менеджере паролей, можно ещё бумажную копию. Бэкап рядом с .env на одном диске это единая точка отказа, копия за пределы сервера остаётся отдельным шагом.
Как перенести сложные пароли на телефон
Пароли по 32-40 случайных символов вручную на iPhone не набрать. Сохранять их надо в менеджер паролей. На iPhone есть встроенное приложение "Пароли" (iOS 18 и новее): бесплатно, синхронизация через iCloud Keychain со сквозным шифрованием, есть на Mac и Windows. Если основной компьютер на Windows или Linux, подойдёт Bitwarden. Сохранить стоит COUCHDB_PASSPHRASE (самое важное, не восстанавливается), пару LIVESYNC_USER и LIVESYNC_PASSWORD для входа с устройств и COUCHDB_PASSWORD для восстановления сервера.
Переношу я их QR-кодами. Маленький скрипт на сервере читает .env и рисует по картинке на ключ. Отдельное окружение Python лежит в каталоге проекта (tools/.venv), а не в системе: там segno для QR и Pillow для подписи, зависимости в tools/requirements.txt. В QR кладётся только значение, чтобы при сканировании копировался чистый пароль без имени, а имя переменной идёт подписью над кодом; для варианта KEY=value есть опция --with-name. Картинки лежат в каталоге с правами 700, файлы 600, ключ --clean удаляет их после сканирования. Скрипт печатает только пути к картинкам, значения никуда не выводит. Дальше камера iPhone, жёлтая подпись, "Скопировать" и вставка в запись "Паролей".
Если работаешь с ИИ-ассистентом в редакторе. В VS Code с расширением Claude выделение текста автоматически уходит ассистенту в контекст. Выделил строку в
.env, чтобы скопировать, и секрет уже в переписке. Копируй без выделения мышью: курсор после=, Shift+End, Ctrl+C, и сразу снять выделение. А самому ассистенту закрыть чтение.envв настройках прав.
Первое устройство: ПК
Кто первый, тот заливает хранилище на сервер. В базе ещё ничего нет, поэтому первым беру устройство, где удобнее вводить данные, то есть ПК. Сначала пробовал iPhone: ввод сложных паролей на телефоне и падающая проверка сервера заставили переключиться.
Хранилище создаётся обычное, в обычной папке, не в OneDrive, Dropbox или iCloud Drive: одно хранилище синхронизируется одним способом. Дальше шестерёнка слева внизу, "Сторонние плагины", включить их, "Обзор", Self-hosted LiveSync, "Установить" и "Включить".
Мастер настройки
Плагин переведён на русский частично. На первом экране выбираю "Я настраиваю это впервые", второй вариант про добавление устройства пригодится позже для телефона.
Экран "Сквозное шифрование": ставлю галочку, в появившемся поле "Парольная фраза" вставляю COUCHDB_PASSPHRASE, только значение, без имени и =, и сверяю края через кнопку показа пароля. Пункт Obfuscate Properties выключен, с ним MCP не найдёт заметки. Advanced не трогаю, алгоритм по умолчанию и E2EE v2 с MCP работают. Плагин честно предупреждает, что passphrase не проверяется до начала синхронизации и ошибка всплывёт только потом, поэтому её надо вставлять, а не набирать.
В способе подключения выбираю "Configure a remote manually" и CouchDB: Setup URI ещё нет, его создаст первое устройство.
На экране CouchDB Configuration: URL https://obsidian.example.com без слеша в конце, имя пользователя livesync (не admin), пароль LIVESYNC_PASSWORD, имя базы obsidian, Use Internal API выключено, CORS на сервере уже настроен. Кнопка Check server requirements скажет "Проверка конфигурации не удалась", и это нормально: проверка читает /_node/_local/_config, а nginx его закрывает, в логе видно 403. Кнопку Fix жать не нужно, реальная проверка это следующая кнопка, Create or connect to database and continue.
Последний экран, "Final Confirmation: Overwrite Server Data with This Device's Files": три галочки, включая "I have created a backup of my Vault", и кнопка I Understand, Overwrite Server.
Ловушка 1: Overwrite Server требует прав администратора CouchDB
Симптом: мастер прошёл, внизу синхронизация стоит на нуле и значок перечёркнут красным, заметки на сервер не уходят. Сохранения не помогают.
Причина видна по логу nginx: "Overwrite Server" сначала удаляет базу (DELETE /obsidian/), а потом создаёт заново. У livesync прав на это нет, отсюда 401. Плагин остаётся в состоянии "первая загрузка не завершена" и ставит себя на паузу.
Лечение такое, что пароль админа на ПК не попадает, а окно риска измеряется минутами. Сначала бэкап и копия каталога couchdb-local.d/. Потом временно делаю livesync администратором сервера:
set -a; . ./.env; set +a
jq -n --arg p "$LIVESYNC_PASSWORD" '$p' | docker exec -i obsidian-couchdb sh -c 'cat > /tmp/p.json'
printf 'user = "%s:%s"\n' "$COUCHDB_USER" "$COUCHDB_PASSWORD" \
| docker exec -i obsidian-couchdb curl -fsS -K - -X PUT --data-binary @/tmp/p.json \
"http://127.0.0.1:5984/_node/_local/_config/admins/${LIVESYNC_USER}"
docker exec obsidian-couchdb rm -f /tmp/p.json
Выдать роль _admin через документ пользователя в _users нельзя, CouchDB отвечает 403. Изменение применяется через пару секунд, проверить можно запросом GET /_session, там должно появиться "roles":["_admin"].
Дальше на ПК: "Настройки", "Self-hosted LiveSync", "Перезапустить мастер настройки" и всё заново. Теперь в логе DELETE /obsidian/ 200, PUT /obsidian/ 201, _bulk_docs 201. Окно "All optional features are disabled" про выключенные Customisation Sync и Hidden File Sync закрываю кнопкой OK: для заметок они не нужны, наборы плагинов на ПК и телефоне всё равно разные.
И сразу снимаю права (префикс с printf и docker exec тот же, что выше):
... curl -K - -X DELETE "http://127.0.0.1:5984/_node/_local/_config/admins/${LIVESYNC_USER}"
Как сделать проще: выдать livesync права администратора до мастера и снять после. Это экономит час.
Ловушка 2: пересозданная база теряет права доступа
После удаления и создания база получает _security по умолчанию, где в members.roles стоит ["_admin"]. Как только livesync перестаёт быть админом, он получает 401 на свою же базу.
Лечение: вернуть _security из бэкапа, вот зачем скрипт сохраняет его отдельно.
docker exec -i obsidian-couchdb sh -c 'cat > /tmp/sec.json' < /var/backups/obsidian/security-YYYY-MM-DD.json
... curl -K - -X PUT -H 'Content-Type: application/json' --data-binary @/tmp/sec.json \
"http://127.0.0.1:5984/obsidian/_security"
Проверка: у livesync своя база отвечает 200, _all_dbs даёт 401, в _session нет _admin.
Ловушка 3: MCP перестал расшифровывать заметки
Симптом: list_notes показывает заметки, read_note отвечает "Note not found", в логах MCP Decryption with HKDF failed.
Причина: у новой базы новая соль шифрования, она лежит в _local/obsidian_livesync_sync_parameters, поле pbkdf2salt. Запущенный MCP помнит старую.
Лечение: пересоздать контейнер MCP командой docker compose up -d --force-recreate mcp. Заодно я поменял VAULT_NAME на реальное имя хранилища. После старта в логах появляется Synchronisation parameters fetched successfully и число проиндексированных заметок.
Ловушка 4: Synchronisation paused for compatibility review
Окно появляется после пересоздания базы: плагин просит обновить плагин на всех устройствах и держит синхронизацию на паузе. На единственном устройстве безопасно нажать Resume synchronisation. Крестик и фиолетовая кнопка Keep synchronisation paused оставляют паузу. Вернуться к окну можно командой Review why synchronisation is paused, но она видна, только пока пауза активна: если в палитре "Команда не найдена", значит паузы уже нет.
Ловушка 5: "Диагностика настроек" ломает синхронизацию
Симптом: режим LiveSync выбран, пауза снята, но заметки не уходят. В логе nginx при каждом сохранении только GET/PUT _local/obsydian_livesync_milestone, без _bulk_docs и _changes. В журнале плагина сначала "Some mismatches have been detected in the configuration between devices. Running a manual replication will attempt to resolve this issue.", а затем "Не удалось подключиться к серверу". Это сообщение ложное, подключение в порядке.
Причина: в Hatch была нажата "Диагностика настроек", и она "исправила" customChunkSize с 0 на 60. В базе записано 0. Автосинхронизация при расхождении молча отказывается работать.
Лечение: палитра команд, Self-hosted LiveSync: Sync now, то есть ручная синхронизация. Плагин сам включает "Automatic alignment of compatible chunk settings", обновляет настройки в базе и запускает LiveSync. Окна с таблицей может и не быть. Не принимай предложения "Диагностики" менять customChunkSize на уже работающей базе.
Ловушка 6: после Fetch и Overwrite автосинхронизация выключена
Перед загрузкой плагин вызывает suspendAllSync() и выключает LiveSync, периодическую синхронизацию, sync on save и sync on start. После загрузки он их не включает обратно, в исходниках CLI-версии плагина так и написано: "suspendAllSync() clobbers them". Итог: синхронизация только по ручной команде.
Лечение: после мастера на каждом устройстве зайти в "Настройки синхронизации" и заново выбрать режим. Похожие жалобы есть в issue плагина.
Как диагностировать
Источника правды два, и это главный навык во всей истории.
Первый, лог nginx /var/log/nginx/obsidian.access.log. По нему видно, есть ли запросы от устройства вообще: User-Agent у ПК выглядит как obsidian/1.13.7 ... Electron, у телефона как iPhone ... obsidian. Дальше по запросам: _bulk_docs с кодом 201 означает, что данные ушли, _changes означает обмен, одни только milestone говорят, что плагин проверил базу и сам остановился, 401 это права, 403 на /_node это закрытая админка.
Второй, журнал самого плагина: палитра команд (Ctrl+P, на iPhone свайп вниз по заметке) и Self-hosted LiveSync: Show log. Названия команд плагина не переводятся, искать надо по-английски: Show log, Sync now.
В строке состояния внизу перечёркнутый красный значок и нули у счётчиков означают, что синхронизация стоит.
Второе устройство: iPhone
Если от неудачной первой попытки осталось хранилище, удали его в "Управлении хранилищами". Новое создаётся с тем же именем, что на ПК, тогда ссылки obsidian:// от ИИ открываются и на телефоне. Пункт "Хранить в iCloud" выключить: iCloud вместе с LiveSync дают дубликаты и конфликты. Плагин ставится так же, как на ПК.
Настройки передаются с ПК из раздела "Для настройки других устройств". Кнопка "Показать QR код" в русском интерфейсе показывает окно "Мы сгенерировали QR-код... Отсканируйте QR-код телефоном" без самого QR-кода, а OK просто закрывает окно. Причина в том, что в русском переводе строки Setup.QRCode потерян плейсхолдер ${qr_image}, в английском он есть. Обход: "Внешний вид", язык плагина English, показать QR, вернуть язык. Важно, что этот QR не зашифрован, в нём все настройки включая пароли, поэтому только со своего экрана, не фотографировать и не пересылать.
Альтернатива это "Копировать текущие настройки в Setup URI". Строка зашифрована паролем, который ты придумываешь сам, её можно переслать себе, например в "Избранное" мессенджера. Setup URI и пароль к нему стоит сохранить в менеджер паролей, пригодятся для следующих устройств.
В мастере на телефоне: "Я добавляю устройство к существующей настройке синхронизации", дальше QR или Setup URI и пароль. На экране "Mostly Complete: Decision Required" нужен вариант "My remote server is already set up. I want to join this device." Не первый, потому что "I am setting up a new server... / reset" перезапишет сервер пустым телефоном, и не третий, плагин сам называет его опасным.
Дальше "Data retrieval scheduled" и Overwrite all with remote files. Overwrite здесь про файлы на iPhone, сервер не трогается. Вариант "Compare time and take newer" не подходит: Obsidian кладёт в каждое новое хранилище заметку "Добро пожаловать", на iPhone она новее серверной и победила бы. Если появится окно про compatibility review, жми Resume.
После Fetch iPhone один раз синхронизировался и замолчал: правка с телефона на ПК не пришла. По логу nginx после Fetch от iPhone не было ни одного запроса. Ручной Sync now отправлял правку, а следующую уже нет. Причина та же, что и на ПК, шестая ловушка: Fetch выключил автосинхронизацию.
Правильные режимы синхронизации
Идти надо в "Настройки", "Self-hosted LiveSync", "Настройки синхронизации", "Режим синхронизации".
| Устройство | Режим | Дополнительно |
|---|---|---|
| ПК | LiveSync (непрерывно) | Keep replication active in the background включить |
| iPhone | LiveSync | Sync on Startup, Sync on File Open, Sync on Save включить |
Даже если в поле уже стоит LiveSync, после мастера выбери его заново, чтобы он применился.
Свёрнутый Obsidian на телефоне замораживается через несколько секунд, это ограничение iOS, а не плагина. Правка, сделанная прямо перед сворачиванием, уйдёт при следующем открытии, через Sync on Startup. В итоге пишешь заметку, и через секунду-две она на сервере и на другом устройстве, без нажатий.
Мелочи, которые стоит знать
Пустые папки не синхронизируются, LiveSync передаёт файлы. Новая папка появится на других устройствах и в MCP, когда в ней будет хоть одна заметка.
Переменная VAULT_NAME должна точно совпадать с именем хранилища в Obsidian, иначе ссылки obsidian://open?vault=... не открываются. Смена имени даёт новый подкаталог в томе MCP: поисковый индекс пересоберётся, а OAuth-клиентов придётся переподключить.
Команда docker compose restart не перечитывает .env, нужен docker compose up -d.
Удаление заметки через MCP уходит в базу, и LiveSync удаляет её на всех устройствах.
Лимиты: документ CouchDB 50 МБ, client_max_body_size 128M. Большие вложения раздувают базу.
Инструкции для ИИ можно держать на сервере (MCP_INSTRUCTIONS_FILE), а запись ограничить папкой (WRITE_FOLDERS), это пригодится при открытии MCP наружу.
Что даёт E2E-шифрование, а что нет
Защищено содержимое заметок: утечка логина с телефона даёт только шифротекст, дамп без passphrase бесполезен.
Не защищены пути и имена файлов, они лежат в базе открытым текстом, вида "path":"папка/заметка.md". Скрыть их можно через Path Obfuscation, но тогда не работает MCP, выбирать приходится. И root на сервере: passphrase лежит в .env для MCP, значит, это уже шифрование при хранении, а не строго end-to-end.
Итоговая проверка
Заметка, созданная на ПК, через 1-2 секунды читается через MCP с сервера, и раз она расшифровывается, то passphrase и Path Obfuscation в порядке. Правка на iPhone приходит на ПК без ручных команд. Пользователь livesync не админ, _all_dbs для него 401, админка снаружи 403. Бэкап по расписанию работает, восстановление проверено.
Что дальше
Открыть MCP наружу через nginx с OAuth (MCP_AUTH_TOKEN, BASE_URL), чтобы Claude в браузере, десктопе и на телефоне мог записывать мысли в Inbox-папку, а запись ограничить через WRITE_FOLDERS. Защиты от перебора пароля на форме входа MCP нет, понадобится limit_req в nginx, при этом fail2ban не должен забанить адреса Claude.
Копия бэкапа за пределы сервера через rsync или rclone. Fail2ban на 401 для CouchDB и ротация логов nginx для этого сайта.