The $96/Year Problem
Obsidian Sync costs $8/month. That’s $96/year to sync markdown files — plain text — across your devices.
I already pay for a VPS. I already run Docker. And I was already staring at a bill for a note-syncing service that, fundamentally, moves .md files between computers.
So I asked myself: how hard could this be?
The answer: harder than it should be. But completely worth it.
My First Attempt Was Embarrassingly Wrong
My first instinct was to run Obsidian itself in a Docker container on the VPS and access it through a browser. No local app needed — just open a tab, take notes.
I got it working. It used 4GB of shared memory, 6GB RAM, and 2 full CPUs. For a note-taking app.
What I had actually built was a remote desktop that happened to have Obsidian open. I was running a browser, inside a VNC session, inside a container, to display an Electron app, to edit plain text files.
Three layers of indirection. Keyboard shortcuts broken on mobile. Copy-paste unreliable. Loading took 10 seconds.
This is a cautionary tale about solving the right problem. I wanted to access my notes everywhere. I built a remote desktop. Those are not the same thing.
The Right Tool: Self-hosted LiveSync
The actual solution is obsidian-livesync — a community plugin that uses CouchDB as a sync relay.
Here’s the mental model:
Your laptop ──┐
Your iPhone ──┼──▶ CouchDB on VPS ◀── bidirectional real-time sync
Your desktop ──┘
CouchDB doesn’t store your notes as files. It stores them as documents in a database. The plugin on each device handles converting between Obsidian’s file format and CouchDB’s document format. Every change syncs within seconds.
The resource profile: ~200MB RAM. That’s it.
What the Setup Actually Looks Like
Server side (one docker-compose.yml)
services:
couchdb:
image: couchdb:3
container_name: obsidian_sync
env_file: .env
volumes:
- /opt/obsidianRemote/couchdb/data:/opt/couchdb/data
- /opt/obsidianRemote/couchdb/config:/opt/couchdb/etc/local.d
restart: unless-stopped
networks:
- npm_default
networks:
npm_default:
external: true
CouchDB joins the same Docker network as Nginx Proxy Manager, which handles SSL termination. The container itself only speaks HTTP internally.
CORS configuration (required for Obsidian)
CouchDB needs to allow connections from Obsidian’s internal origins. Drop this in as local.ini:
[httpd]
enable_cors = true
[cors]
origins = app://obsidian.md,capacitor://localhost,http://localhost
credentials = true
methods = GET, PUT, POST, HEAD, DELETE
headers = accept, authorization, content-type, origin, referer
app://obsidian.md is how the Obsidian desktop app identifies itself. capacitor://localhost is the iOS/Android wrapper. Without these, every sync request gets rejected.
Initialize the databases
CouchDB on first start has no system databases. Create them:
docker exec obsidian_sync curl -sf -u "$USER:$PASS" -X PUT http://localhost:5984/_users
docker exec obsidian_sync curl -sf -u "$USER:$PASS" -X PUT http://localhost:5984/_replicator
docker exec obsidian_sync curl -sf -u "$USER:$PASS" -X PUT http://localhost:5984/_global_changes
docker exec obsidian_sync curl -sf -u "$USER:$PASS" -X PUT http://localhost:5984/obsidian
The obsidian database is where your vault lives.
The Mistakes I Made (So You Don’t Have To)
1. docker compose vs docker-compose
My VPS runs Docker Compose v1 (docker-compose with a hyphen). The v2 command (docker compose as a subcommand) doesn’t exist there. The deploy silently failed until I checked the logs.
Always verify which version of Compose your server has before writing CI/CD pipelines.
2. Passwords with special characters in URLs
My CouchDB password contained @ and %. When I embedded credentials in a curl URL like http://user:p@ss@host, the @ in the password was parsed as the URL’s host separator. The URL was malformed. Every curl command silently failed.
Fix: always use curl -u "user:password" instead of embedding credentials in the URL.
3. The Cloudflare 525 error
HTTP 525 means Cloudflare can’t complete an SSL handshake with your origin. In my case, I had accidentally set the NPM proxy scheme to https instead of http. NPM was trying to open an SSL connection to a CouchDB container that only speaks HTTP.
NPM handles SSL externally. Everything behind it is plain HTTP.
4. The image pull timeout
The first docker-compose pull for CouchDB timed out at 15% extraction. The default SSH action timeout in GitHub Actions is 10 minutes. A first-time pull on a slow VPS takes longer.
Fix: set command_timeout: 30m on the pull step. After the first pull, the image is cached and subsequent deploys are fast.
Adding a Second Device
The livesync plugin has a clever setup-sharing feature. On your first device (after setup), go to plugin settings and generate a QR code. On the second device, scan it — all connection settings transfer instantly. No re-entering URLs or passwords.
On the new device, choose “Overwrite local with server data” — it pulls the full vault from CouchDB. The first device should choose “Overwrite server with local” to establish the initial state.
The Real-Time Connection Problem on Mobile
LiveSync uses CouchDB’s _changes feed for real-time sync — a long-lived HTTP connection that streams changes as they happen. Cloudflare kills these connections after ~100 seconds. When the connection drops, the plugin falls back to periodic one-shot syncs.
This is why you might see the log pattern:
LiveSync begin...
Replication closed ← Cloudflare killed the connection
OneShot Sync begin... (pullOnly)
Replication completed
LiveSync begin... ← tries again, fails again
The workaround: in plugin settings, enable Periodic sync (every 5 minutes), Sync on file save, and Sync on startup. You lose true real-time sync but gain reliable automatic sync.
What I Have Now
- Notes sync between Windows laptop and iPhone in under 30 seconds
- Server uses ~200MB RAM (down from a theoretical 6GB)
- Vault data stays on my VPS — no third-party services
- Deployment is a single
git pushvia GitHub Actions - The whole server-side config is 12 lines of YAML
The $96/year stays in my pocket. More importantly: I understand exactly how my notes move between devices, what touches them, and where they live.
The Takeaway
If you already run a VPS: self-hosted LiveSync is the right answer. CouchDB is genuinely lightweight, the plugin is mature, and the setup is a weekend afternoon.
If you don’t run a VPS: just pay for Obsidian Sync. The complexity isn’t worth it for the savings.
The mistake I almost committed — and the one worth remembering — is that “access my notes from anywhere” and “run Obsidian on a server” are different problems. The first one needs a sync relay. The second one needs a remote desktop. Know which problem you’re solving before you build.
All config files and the GitHub Actions workflow for this setup are on GitHub.