Obsidian 동기화 서버를 직접 만들었다: Self-hosted LiveSync와 CouchDB

Obsidian을 쓰다 보면 결국 동기화 문제를 만난다. Mac 한 대에서만 쓸 때는 별일이 없다. 볼트 폴더 안의 Markdown 파일을 Obsidian이 읽어 주는 단순한 구조니까. 하지만 iPhone과 iPad가 추가되고, 월 구독 없이 여러 기기를 연결하고 싶어지면 이야기가 달라진다.

나도 처음에는 iCloud Drive를 사용했다. Apple 기기끼리는 가장 자연스러운 선택이었다. 별도 플러그인도 필요 없고, Obsidian에서 iCloud 볼트를 만들면 Mac과 iPhone에서 바로 사용할 수 있다.

문제는 내가 운영하는 Ubuntu 서버였다. 서버를 개인 서비스와 자동화의 중심으로 쓰고 있는데, Linux에서 iCloud Drive를 안정적인 상시 동기화 저장소처럼 다루기는 어렵다. Syncthing은 Mac과 Linux에서는 훌륭하지만 iOS가 걸렸다. Git은 기록과 복구에는 강하지만 실시간 동기화 도구로 쓰기엔 손이 많이 간다. 공식 Obsidian Sync는 가장 편한 답이지만 매달 비용이 든다.

결국 내 조건은 이랬다.

  • MacBook, iPhone, iPad에서 같은 볼트를 쓰고 싶다.
  • 특정 회사의 클라우드 저장소에 묶이고 싶지 않다.
  • 이미 운영 중인 Ubuntu 서버와 Docker 환경을 활용하고 싶다.
  • 동기화 서버를 내가 직접 운영하고 싶다.
  • 가능하면 추가 구독료 없이 구성하고 싶다.

이 조건을 가장 잘 맞춘 조합이 Self-hosted LiveSync와 CouchDB였다.

이 글은 Ubuntu 서버에 CouchDB를 Docker로 실행하고, Cloudflare Tunnel을 거쳐 Mac·iPhone·iPad의 Obsidian을 연결한 실제 구성 과정을 정리한 글이다.

먼저 알아둘 것: 이건 파일 복사가 아니다

Self-hosted LiveSync를 이해하려면 Dropbox나 iCloud와 같은 파일 동기화 서비스부터 잠시 잊는 편이 좋다.

일반적인 파일 동기화는 완성된 파일을 기기 사이에서 복사한다. 반면 Self-hosted LiveSync는 Obsidian 플러그인이 각 기기의 변경 내용을 감지하고, 이를 CouchDB 문서와 청크 형태로 저장·복제한다.

MacBook Obsidian ─┐
                  │
iPhone Obsidian ──┼── HTTPS ── Cloudflare Tunnel ── CouchDB
                  │
iPad Obsidian ────┘

각 기기에는 평소처럼 실제 Markdown 볼트가 있다. 사용자는 로컬 파일을 열고 수정한다. Self-hosted LiveSync 플러그인이 변경 사항을 CouchDB로 보내고, 다른 기기들은 CouchDB에서 새 변경 사항을 받아 자기 로컬 볼트에 반영한다.

CouchDB가 이 구조에 잘 맞는 이유도 여기에 있다. CouchDB는 JSON 문서 저장뿐 아니라 문서 버전과 변경 이력, 복제를 핵심 기능으로 제공한다. 네트워크가 잠시 끊겼다가 다시 연결되는 모바일 기기와 궁합이 좋다.

Ubuntu 서버에 Markdown 파일이 생기는 것은 아니다

이 부분은 꼭 짚고 넘어가야 한다.

CouchDB의 Docker 볼륨을 열어도 내 노트.md 같은 파일은 보이지 않는다. 서버에는 CouchDB 데이터 파일이 저장될 뿐이다. Markdown 원본은 각 Obsidian 클라이언트의 로컬 볼트에 있다.

각 기기:      실제 .md 파일
Ubuntu 서버: CouchDB 문서와 첨부파일 청크

따라서 서버의 CouchDB 데이터 디렉터리를 직접 수정하면 안 된다. Ubuntu에서 Markdown 파일을 직접 읽고 수정해야 한다면 별도의 Obsidian 클라이언트나 LiveSync 호환 브리지가 필요하다. 이번 구성에서 Ubuntu는 동기화 중계 서버 역할만 맡는다.

다른 방법 대신 이 구성을 고른 이유

처음부터 Self-hosted LiveSync만 본 것은 아니다. 후보마다 장단점이 뚜렷했다.

방식 장점 내가 포기한 이유
iCloud Drive Apple 기기에서 가장 간단함 Ubuntu/Linux 연동이 애매하고 이중 동기화 위험이 있음
Obsidian Sync 공식 지원, 설정과 복구가 편함 계속 발생하는 구독 비용
Syncthing 무료, 빠름, Mac·Linux에서 안정적 iOS 백그라운드 동기화와 앱 제약
Git 변경 이력과 복구에 강함 실시간 동기화가 아니며 모바일 충돌 처리가 번거로움
Remotely Save S3·WebDAV 등 선택지가 많음 즉시 복제보다는 주기적 동기화에 가깝고 별도 저장소가 필요함
Self-hosted LiveSync 무료, 여러 플랫폼 지원, 거의 실시간, 서버 소유권 확보 설치와 장애 복구를 직접 책임져야 함

공식 Obsidian Sync가 나쁜 선택이라는 뜻은 아니다. 오히려 서버 운영에 시간을 쓰고 싶지 않다면 가장 합리적이다. 공식 서비스 비용에는 동기화 서버, 버전 기록, 장애 대응, 보안 업데이트, 고객 지원이 포함된다.

나는 이미 24시간 돌아가는 Ubuntu 서버와 Docker, Cloudflare Tunnel을 운영하고 있었다. 새 서버 비용이 들지 않았고, 장애가 생겼을 때 직접 확인할 수도 있었다. 그래서 편의성을 조금 포기하고 운영권을 가져오는 쪽을 골랐다.

최종 아키텍처

내가 구성한 전체 경로는 다음과 같다.

┌──────────────────┐
│ MacBook Obsidian │  ← 최초 업로드를 담당하는 기준 기기
│ + LiveSync       │
└────────┬─────────┘
         │
┌────────▼─────────┐
│ iPhone / iPad    │  ← 기존 원격 DB에 참가하는 보조 기기
│ Obsidian         │
│ + LiveSync       │
└────────┬─────────┘
         │ HTTPS
         ▼
┌──────────────────────────────┐
│ Cloudflare                   │
│ Public Hostname              │
│ obsidian-sync.example.com    │
└──────────────┬───────────────┘
               │ Named Tunnel
               ▼
┌──────────────────────────────┐
│ Ubuntu                       │
│ cloudflared                  │
│        ↓                     │
│ 127.0.0.1:5984               │
│        ↓                     │
│ CouchDB 3.5 Docker Container │
│        ↓                     │
│ Docker Named Volumes         │
└──────────────────────────────┘

여기서 중요한 보안 경계는 127.0.0.1:5984다. CouchDB 포트를 인터넷이나 내부 네트워크 전체에 직접 열지 않았다. 서버 로컬에서만 접속할 수 있게 묶고, 외부 기기는 Cloudflare Tunnel의 HTTPS 주소를 사용한다.

인터넷 → Cloudflare HTTPS → 암호화된 Tunnel → localhost:5984

공유기에서 5984 포트를 포워딩할 필요도 없고, 서버 방화벽에 CouchDB 포트를 공개할 이유도 없다.

준비물

구성을 시작하기 전에 아래 항목을 준비했다.

  • 24시간 실행되는 Ubuntu 서버
  • Docker Engine과 Docker Compose 플러그인
  • 사용할 도메인
  • Cloudflare에서 관리 중인 DNS
  • 서버에서 실행 중인 Named Tunnel
  • MacBook, iPhone, iPad의 Obsidian
  • 각 기기에 설치할 Self-hosted LiveSync 커뮤니티 플러그인
  • 비밀번호 관리자

CouchDB 계정 비밀번호와 LiveSync의 E2EE 암호는 서로 다른 값이다. 둘 다 반드시 비밀번호 관리자에 보관해야 한다.

1. CouchDB 디렉터리 만들기

서버의 Docker 앱은 한곳에서 관리하는 편이 좋다. 나는 다음 경로를 사용했다.

mkdir -p ~/docker/obsidian-livesync
cd ~/docker/obsidian-livesync

최종 파일 구조는 단순하다.

~/docker/obsidian-livesync/
├── docker-compose.yml
├── .env
└── couchdb-init.sh

.env에는 비밀값이 들어가므로 Git에 커밋하지 않는다.

2. CouchDB 계정 만들기

무작위 비밀번호를 만든다.

python3 -c 'import secrets; print(secrets.token_urlsafe(48))'

생성된 값을 이용해 .env를 작성한다.

COUCHDB_USER=obsidian_admin
COUCHDB_PASSWORD=충분히_긴_무작위_비밀번호

권한도 제한한다.

chmod 600 .env

블로그나 저장소에 실제 .env를 올리면 안 된다. LiveSync 설정 URI 역시 CouchDB 주소와 인증 정보가 들어갈 수 있으므로 공개하면 안 된다.

3. Docker Compose 작성하기

실제로 사용한 구성은 아래와 같다.

services:
  couchdb:
    image: couchdb:3.5
    container_name: obsidian-livesync-couchdb
    restart: unless-stopped
    environment:
      COUCHDB_USER: ${COUCHDB_USER}
      COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
    volumes:
      - couchdb-data:/opt/couchdb/data
      - couchdb-config:/opt/couchdb/etc/local.d
    ports:
      - "127.0.0.1:5984:5984"
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          curl -fsS -u "$$COUCHDB_USER:$$COUCHDB_PASSWORD"
          http://127.0.0.1:5984/_up || exit 1
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 20s

volumes:
  couchdb-data:
  couchdb-config:

latest가 아니라 3.5인가

서버 애플리케이션에서 latest는 편하지만 예측하기 어렵다. 어느 날 컨테이너를 다시 만들었는데 메이저 버전까지 바뀔 수 있다. 최소한 메이저·마이너 계열은 고정해 두는 편이 낫다.

왜 볼륨이 두 개인가

  • couchdb-data: 실제 데이터베이스 데이터
  • couchdb-config: LiveSync 초기화 스크립트가 적용한 CouchDB 설정

컨테이너를 삭제하고 새 이미지로 다시 만들어도 두 볼륨이 남아 있으면 데이터와 설정을 이어서 사용할 수 있다. 물론 볼륨이 백업을 대신해 주지는 않는다.

health check의 $$는 오타가 아니다

Compose 파일에서 $COUCHDB_USER라고 쓰면 호스트의 Compose가 먼저 변수 치환을 시도한다. $$COUCHDB_USER라고 적어야 컨테이너 안에서 실제 환경 변수를 읽는다.

이 차이는 뒤에서 꽤 중요한 문제를 막아 줬다.

4. CouchDB 시작하기

cd ~/docker/obsidian-livesync
docker compose up -d

상태를 확인한다.

docker compose ps

정상이라면 healthy가 표시된다.

로컬 API도 확인한다.

set -a
source .env
set +a

curl -fsS \
  -u "$COUCHDB_USER:$COUCHDB_PASSWORD" \
  http://127.0.0.1:5984/_up

예상 응답은 다음과 같다.

{"status":"ok"}

버전도 확인할 수 있다.

curl -fsS \
  -u "$COUCHDB_USER:$COUCHDB_PASSWORD" \
  http://127.0.0.1:5984/

내가 글을 쓸 때 실제 서버에서 실행 중인 버전은 CouchDB 3.5.2였다.

5. LiveSync용 CouchDB 초기화

CouchDB 컨테이너를 띄우는 것만으로는 끝나지 않는다. Self-hosted LiveSync가 브라우저 기반 Obsidian 앱과 통신하려면 인증과 CORS 설정이 필요하다.

프로젝트에서 제공하는 초기화 스크립트를 받는다.

curl -fsSLO \
  https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh
chmod 700 couchdb-init.sh

인터넷에서 받은 스크립트는 바로 실행하기 전에 내용을 확인하는 습관이 좋다.

less couchdb-init.sh

그다음 .env 값을 읽어 초기화한다.

set -a
source .env
set +a

hostname=http://127.0.0.1:5984 \
username="$COUCHDB_USER" \
password="$COUCHDB_PASSWORD" \
bash ./couchdb-init.sh

초기화 스크립트는 LiveSync에 필요한 CORS, 인증, 데이터베이스 동작 관련 설정을 적용한다.

확인할 핵심은 다음과 같다.

  • 인증하지 않은 사용자는 접근할 수 없어야 한다.
  • enable_cors가 활성화되어야 한다.
  • Obsidian 데스크톱과 모바일 앱의 origin이 허용되어야 한다.
  • 인증된 /_up 요청은 계속 성공해야 한다.

우리가 실제로 만난 문제: 초기화 후 컨테이너가 unhealthy가 됐다

처음에는 health check를 익명 요청으로 작성하기 쉽다.

healthcheck:
  test: ["CMD", "curl", "-f", "http://127.0.0.1:5984/_up"]

CouchDB를 막 띄운 직후에는 이 요청이 성공할 수 있다. 그런데 LiveSync 초기화가 require_valid_user를 활성화하면 익명 /_up 요청은 HTTP 401을 반환한다. CouchDB는 멀쩡한데 Docker는 컨테이너를 unhealthy로 표시한다.

해결 방법은 health check에도 인증 정보를 넣는 것이다.

healthcheck:
  test:
    - CMD-SHELL
    - >-
      curl -fsS -u "$$COUCHDB_USER:$$COUCHDB_PASSWORD"
      http://127.0.0.1:5984/_up || exit 1

수정 후 컨테이너를 다시 만든다.

docker compose up -d --force-recreate

CouchDB 장애가 아니라 보안 설정이 강화되면서 기존 점검 방식이 틀어진 것이었다. 원인을 모르고 볼륨까지 지웠다면 데이터만 잃을 뻔했다.

6. CouchDB 포트가 로컬에만 열렸는지 확인하기

ss -ltnp | grep 5984

원하는 결과는 아래처럼 127.0.0.1에만 바인딩된 상태다.

127.0.0.1:5984

아래처럼 나오면 외부 인터페이스에도 열려 있다는 뜻이다.

0.0.0.0:5984
[::]:5984

이 경우 Compose의 포트 매핑을 다시 확인해야 한다.

7. Cloudflare Tunnel 연결하기

모바일 기기는 집 밖에서도 동기화해야 한다. Tailscale처럼 각 기기에 VPN을 연결하는 대신, 기존에 운영하던 Cloudflare Named Tunnel에 전용 Public Hostname을 추가했다.

Cloudflare Zero Trust에서 다음 경로로 이동한다.

Networks
→ Tunnels
→ 사용 중인 Tunnel
→ Public Hostnames
→ Add a public hostname

예시는 다음과 같다.

Subdomain: obsidian-sync
Domain: example.com
Type: HTTP
URL: localhost:5984

완성 주소는 다음과 같은 HTTPS URL이 된다.

https://obsidian-sync.example.com

cloudflared tunnel run --token ... 방식으로 실행 중인 remotely managed tunnel이라면 로컬 config.yml이 라우팅의 기준이 아니다. Cloudflare 대시보드나 API에서 Public Hostname을 관리해야 한다.

Cloudflare Access를 붙이지 않은 이유

브라우저용 관리 페이지라면 Cloudflare Access가 매우 유용하다. 하지만 LiveSync 클라이언트 앞에 브라우저 로그인 페이지가 끼면 CouchDB 요청이 리디렉션되어 동기화가 실패할 수 있다.

이 구성에서는 다음 보안 계층을 사용했다.

  1. Cloudflare Tunnel로 원본 서버 포트 비공개
  2. HTTPS 전송 암호화
  3. CouchDB 사용자 인증
  4. LiveSync E2EE
  5. 필요하면 경로와 속성 이름 난독화

Access를 반드시 사용하고 싶다면 서비스 토큰 방식까지 포함해 실제 클라이언트 호환성을 먼저 검증해야 한다.

8. 외부 주소 검증하기

Tunnel을 만들었다고 끝난 것이 아니다. 서버 외부에서 실제 HTTPS 요청이 성공해야 한다.

curl -fsS \
  -u 'obsidian_admin:비밀번호' \
  https://obsidian-sync.example.com/_up

CORS preflight도 확인하면 좋다.

curl -i -X OPTIONS \
  -H 'Origin: app://obsidian.md' \
  -H 'Access-Control-Request-Method: GET' \
  https://obsidian-sync.example.com/

실제 주소와 인증 정보는 셸 기록에 남을 수 있다. 운영 환경에서는 비밀번호를 명령행에 직접 적기보다 임시 환경 변수나 안전한 자격 증명 파일을 사용하는 편이 낫다.

9. MacBook을 기준 기기로 연결하기

여러 기기를 한꺼번에 붙이면 문제가 생겼을 때 원인을 찾기 어렵다. 기존 노트가 있는 MacBook을 기준 기기로 정하고, 여기서 서버의 첫 상태를 만든 뒤 모바일 기기를 하나씩 추가했다.

시작하기 전 볼트 전체를 반드시 백업한다.

기존 Mac 볼트
→ 별도 폴더나 외장 저장소에 전체 복사
→ 백업본은 동기화 대상에서 제외

그리고 iCloud와 LiveSync가 같은 볼트를 동시에 동기화하지 않도록 한다. 하나의 볼트에 동기화 엔진 두 개를 겹치면 삭제, 이름 변경, 설정 파일 변경이 서로 전파되며 예측하기 어려운 충돌이 생길 수 있다.

플러그인 설치

Obsidian에서 다음 순서로 설치한다.

Settings
→ Community plugins
→ Browse
→ Self-hosted LiveSync 검색
→ Install
→ Enable

플러그인 설정에서 다음 값을 입력한다.

URI: https://obsidian-sync.example.com
Username: CouchDB 사용자명
Password: CouchDB 비밀번호
Database name: 영문 소문자 데이터베이스명

데이터베이스 이름은 소문자로 단순하게 정하는 편이 좋다.

my-vault

Detect and Fix CouchDB Issues

첫 동기화 전에 플러그인의 Detect and Fix CouchDB Issues를 실행한다. 보통 다음 권장값을 확인하게 된다.

  • case-sensitive filenames: false
  • per-file saved customisation sync: true
  • CouchDB v3 chunk size enhancement: 60

새로 만든 빈 데이터베이스에서는 원격 설정이 아직 없으므로 Fetch Remote Configuration Failed가 나타날 수 있다. 정말 빈 DB를 처음 구성하는 상황이라면 Skip and proceed로 넘어가도 된다. 이미 사용 중인 DB에서 같은 오류가 난다면 네트워크나 인증 문제부터 확인해야 한다.

첫 업로드

서버 DB가 비어 있고 Mac의 볼트가 유일한 원본이라는 사실을 다시 확인한 뒤, 새 서버를 설정하는 흐름을 선택한다.

Setting up a new server / Reset existing server
→ Overwrite Server with This Device's Files

이 버튼은 평소에 누르는 동기화 버튼이 아니다. 서버를 이 기기의 상태로 덮어쓰는 일회성 작업이다. 다른 기기가 이미 연결된 뒤에는 함부로 사용하면 안 된다.

Do you want to send all chunks before replication? 질문에는 일반적인 초기 설정이라면 기본값인 No를 선택했다.

마지막으로 Setup → Enable LiveSync를 눌러야 실제 복제가 시작된다. 서버 탐지와 DB 생성이 성공했더라도 아래 상태라면 아직 동기화가 꺼져 있다.

No replicator has been activated or has not been initialized yet
↑0 ↓0

10. E2EE 설정하기

Self-hosted LiveSync는 동기화 데이터를 클라이언트에서 암호화한 뒤 CouchDB에 저장하는 E2EE를 지원한다.

여기에는 세 종류의 비밀값이 등장한다.

용도
CouchDB 비밀번호 서버 API 인증
LiveSync E2EE 암호 노트 내용을 암호화·복호화
Setup URI 암호 설정 URI를 다른 기기로 전달할 때 보호

서로 같은 것이 아니다. 특히 E2EE 암호는 서버의 .env에 저장되지 않는다. 잃어버리면 새 기기에서 CouchDB 데이터를 받아도 내용을 복호화할 수 없다.

나는 E2EE 암호를 비밀번호 관리자에 따로 보관했다. 선택적으로 경로와 속성 이름 난독화도 활성화할 수 있지만, 모든 기기에서 같은 설정을 사용해야 한다.

11. iPhone과 iPad 연결하기

모바일에서는 새 빈 볼트를 만든다. LiveSync를 사용할 볼트에 iCloud의 Store in iCloud를 함께 켜지 않는다.

모바일 기기는 서버를 덮어쓰는 기준 기기가 아니다. 반드시 기존 원격 서버에 참가하는 흐름을 선택한다.

Existing configuration / Join this device

Mac과 동일한 값을 입력한다.

  • CouchDB HTTPS URL
  • 데이터베이스 이름
  • CouchDB 사용자명과 비밀번호
  • LiveSync E2EE 암호
  • 경로·속성 난독화 사용 여부

빈 모바일 볼트라면 원격 서버가 정답이다.

Overwrite all with remote files
→ Delete local files if not on remote

이 선택은 모바일의 빈 로컬 볼트를 원격 상태로 맞추는 작업이다. 서버를 덮어쓰는 것이 아니다.

첫 동기화 동안에는 Obsidian을 화면에 열어 두는 편이 좋다. iOS는 앱의 백그라운드 실행 시간을 제한하므로 대용량 볼트를 처음 받을 때 앱을 닫으면 작업이 중단될 수 있다. Wi-Fi와 충전 환경에서 진행하면 안정적이다.

P2P를 사용하지 않는다면 P2P Status의 Disconnected는 문제가 아니다. 이번 구성의 전송 방식은 CouchDB replication이다.

12. 테마와 플러그인 설정까지 동기화하기

노트와 첨부파일만 동기화하면 끝이라고 생각했지만, 실제 사용에서는 테마·플러그인·설정도 중요했다. Self-hosted LiveSync에는 Customisation Sync가 있다.

여기서 가장 큰 함정은 LiveSync 플러그인 자신의 설정까지 다른 기기로 덮어쓸 수 있다는 점이다. Mac의 장치 이름과 연결 상태를 iPhone에 그대로 적용하면 모바일 기기의 고유 설정이 깨질 수 있다.

나는 다음 원칙으로 구성했다.

  1. Mac과 iPhone/iPad에 서로 다른 장치 이름을 지정한다.
  2. 기준 Mac에서 Customisation Sync를 활성화한다.
  3. Beta3 관리 화면에서 변경 사항을 스캔한다.
  4. Self-hosted LiveSync의 MAINDATA를 Ignore/Paused로 설정한다.
  5. Mac에서 Sync once로 설정을 게시한다.
  6. 모바일에서 Sync once → Refresh를 실행한다.
  7. 원하는 테마와 플러그인 설정만 선택해 적용한다.

Select All ShinyApply All Selected를 무조건 누르는 방식은 피했다. 데스크톱 전용 플러그인이나 workspace.json을 모바일에 적용하면 화면 배치와 플러그인 상태가 망가질 수 있다. workspace-mobile.json 역시 모바일 쪽에 남겨 두는 편이 안전하다.

Customisation Sync와 일반 노트 복제는 별개의 기능이다.

일반 LiveSync       → 노트와 첨부파일
Customisation Sync  → 테마, 플러그인, Obsidian 설정
LiveSync MAIN/DATA  → 각 장치의 연결·상태 정보

이 세 가지를 한 덩어리로 생각하면 복구가 어려워진다.

우리가 겪은 복구 문제: 원격 DB가 재구축됐다는 잠금

설정을 다시 만들거나 서버 DB를 재구축하면 보조 기기에서 다음과 비슷한 경고를 볼 수 있다.

The remote database has been rebuilt

이 잠금은 고장이 아니라 안전장치다. 원격 DB의 정체성이 바뀌었는데 기존 로컬 상태로 계속 복제하면 대량 삭제나 중복이 발생할 수 있으므로 플러그인이 동기화를 멈춘다.

보조 기기에 동기화되지 않은 고유 노트가 없고 원격 서버가 기준 상태라면 다음 순서로 복구한다.

Fetch everything again from remote
→ Overwrite all with remote files
→ Delete local files if not on remote

단순히 Unlock remote database만 누르는 것은 권하지 않는다. 로컬과 원격이 정말 같은 상태라는 확신이 없다면 전체 재수신이 안전하다.

Customisation Sync만 초기화하려는 경우에는 일반 LiveSync 연결까지 지우면 안 된다.

Hatch
→ Reset
→ Delete all customization sync data

Back to non-configured는 LiveSync 연결 자체를 초기화하므로 목적이 다르다.

운영하면서 확인할 명령

컨테이너 상태

cd ~/docker/obsidian-livesync
docker compose ps

최근 로그

docker logs --tail 100 obsidian-livesync-couchdb

인증된 health check

set -a
source .env
set +a

curl -fsS \
  -u "$COUCHDB_USER:$COUCHDB_PASSWORD" \
  http://127.0.0.1:5984/_up

볼륨 확인

docker volume ls | grep obsidian-livesync

포트 노출 확인

ss -ltnp | grep 5984

이미지 업데이트

바로 재생성하기 전에 백업과 변경 사항을 확인한다.

cd ~/docker/obsidian-livesync
docker compose pull
docker compose up -d

업데이트 후에는 반드시 docker compose ps, /_up, 외부 HTTPS 주소를 다시 확인한다.

백업은 어떻게 해야 할까

Named Volume은 컨테이너와 데이터를 분리할 뿐 백업이 아니다. 서버 디스크가 망가지면 볼륨도 함께 사라진다.

최소한 두 종류의 백업이 필요하다.

  1. 기준 기기의 Obsidian 볼트 백업
  2. CouchDB 데이터 또는 데이터베이스 복제 백업

가장 중요한 것은 기준 기기의 실제 Markdown 볼트다. E2EE를 사용하는 CouchDB 데이터만 백업해 두고 암호를 잃어버리면 복구할 수 없다.

운영 환경에서는 CouchDB의 replication 기능으로 별도 CouchDB 인스턴스에 복제하거나, 컨테이너를 안전하게 멈춘 뒤 볼륨을 스냅샷하는 방식을 고려할 수 있다. 어떤 방식을 택하든 복원 테스트를 해 보지 않은 백업은 절반짜리다.

이 방식이 모든 사람에게 맞지는 않는다

Self-hosted LiveSync는 꽤 만족스럽지만 공짜 Obsidian Sync 복제품이라고만 생각하면 실망할 수 있다.

내가 직접 책임져야 하는 것이 늘어난다.

  • Ubuntu와 Docker 업데이트
  • CouchDB 보안 업데이트
  • Cloudflare Tunnel 장애
  • 인증 정보와 E2EE 암호 보관
  • 볼륨 및 볼트 백업
  • 기기 추가와 충돌 복구
  • 커뮤니티 플러그인 업데이트 확인

반대로 얻는 것도 분명하다.

  • 동기화 서버의 위치와 운영 방식을 내가 결정한다.
  • 이미 가진 서버를 활용하면 추가 비용이 거의 없다.
  • Mac, iPhone, iPad 사이에서 거의 실시간으로 동기화된다.
  • 각 기기의 볼트는 여전히 로컬 Markdown 파일이다.
  • CouchDB 인증과 LiveSync E2EE를 함께 사용할 수 있다.

서버를 운영하고 싶지 않다면 공식 Obsidian Sync가 더 싸게 먹힐 수 있다. 시간을 비용으로 계산하면 특히 그렇다. 하지만 이미 홈서버를 운영하고 있고, 문제를 직접 추적하는 과정까지 즐긴다면 Self-hosted LiveSync는 좋은 선택이다.

마치며

이번 작업에서 가장 어려웠던 부분은 CouchDB 컨테이너를 띄우는 일이 아니었다. docker compose up -d는 몇 분이면 끝난다. 진짜 중요한 일은 어느 기기를 원본으로 삼을지 정하고, 동기화 엔진을 겹치지 않으며, 모바일 기기를 하나씩 안전하게 참가시키는 것이었다.

그리고 세 가지를 분리해서 이해해야 했다.

노트 동기화
설정 동기화
LiveSync 플러그인 자체의 장치별 설정

이 구분만 지켜도 대부분의 사고를 피할 수 있다.

지금 내 서버에서는 CouchDB 3.5가 Docker로 실행되고 있다. 포트는 127.0.0.1:5984에만 열려 있고, 외부에서는 Cloudflare Tunnel의 HTTPS 주소로 접근한다. MacBook이 기준 기기이고 iPhone과 iPad는 같은 원격 DB에 참가한다. LiveSync의 MAINDATA는 설정 동기화 대상에서 제외했다.

결국 내가 원했던 것은 단순히 무료 동기화가 아니었다. 내 노트는 각 기기의 로컬 Markdown으로 남기고, 그 사이를 연결하는 서버도 내가 운영하는 구조였다. 약간의 수고를 감수한 대신, 데이터가 어디를 지나고 어떻게 저장되는지 설명할 수 있게 됐다.

참고 자료

Posts