Files
obsidian-livesync/docker/README.md
T
mugenrei ebbac96b16 docs: add comprehensive Cloudflare Tunnel setup guide
- Step-by-step tunnel creation in Zero Trust dashboard
- Detailed public hostname configuration (couchdb:5984 vs localhost)
- Troubleshooting section for 404, 502, and 524 errors
- Obsidian plugin configuration instructions
- Emphasis on token-based tunnels ignoring local config file
2026-02-18 10:57:18 -03:00

11 KiB

Self-hosted LiveSync — Docker Setup

A fully self-hosted CouchDB stack for the obsidian-livesync plugin.
No fly.io. No IBM Cloudant. No cloud accounts required for basic use.

Tested on Docker Desktop for Windows (Docker 29.2, Compose v5, WSL2 backend) — full init, CORS, auth, and idempotent restart verified.


Architecture

Obsidian (desktop / iOS / Android)
        │  CouchDB Replication Protocol (HTTPS)
        ▼
[ Reverse Proxy / Tunnel ]  ◄── Choose ONE profile below
        │
        ▼
[ CouchDB container ]  ◄── The only required service
        │  initialized once by couchdb-init container
        ▼
[ Named Docker Volume ]  ◄── All vault data stored here

Quick Start

1. Prerequisites

  • Docker Desktop (Windows/Mac/Linux) or Docker Engine + Compose plugin
  • A machine that Obsidian devices can reach over HTTPS (see profiles below)

2. Configure

cd docker/
cp .env.example .env
# Edit .env — at minimum set COUCHDB_USER and a strong COUCHDB_PASSWORD

3. Launch

# Default: CouchDB only (LAN / localhost, no TLS)
docker compose up -d

# With Caddy (public domain + auto Let's Encrypt)
docker compose --profile caddy up -d

# With Tailscale (no domain needed, private mesh or public Funnel)
docker compose --profile tailscale up -d

# With Cloudflare Tunnel (Cloudflare account required)
docker compose --profile cloudflare up -d

4. Verify

# Should return {"status":"ok"}
curl -u admin:yourpassword http://localhost:5984/_up

# Check CORS headers
curl -v -H "Origin: app://obsidian.md" \
     -u admin:yourpassword \
     http://localhost:5984/

5. Connect Obsidian

In the Obsidian plugin settings (Self-hosted LiveSync):

Field Value
URI https://your-domain-or-ts-hostname:5984 (or http://localhost:5984 for LAN-only)
Username value of COUCHDB_USER
Password value of COUCHDB_PASSWORD
Database name value of COUCHDB_DATABASE (default: obsidiannotes)
End-to-end passphrase your own chosen passphrase — never stored server-side

Profile Details

Default (no profile) — LAN / Localhost only

CouchDB is exposed on http://localhost:5984 (or LAN IP).
Desktop Obsidian works over HTTP. Mobile Obsidian requires HTTPS — use a tunnel profile.

--profile caddy — Public Domain + Auto TLS

Requires:

  • A domain with an A record pointing to this server's public IP
  • Ports 80 and 443 open in your firewall/router

Set in .env:

COUCHDB_DOMAIN=couchdb.yourdomain.com
ACME_EMAIL=you@example.com

Caddy automatically issues a Let's Encrypt certificate. No manual cert management.

Requires:

Set in .env:

TS_AUTHKEY=tskey-auth-...
TS_HOSTNAME=livesync

Two sub-modes:

  • VPN mode (default): CouchDB accessible only to devices on your Tailnet at
    https://livesync.<tailnet>.ts.net — completely private
  • Funnel mode: public HTTPS at https://livesync.<tailnet>.ts.net — no domain purchase
    Enable in your Tailscale ACL:
    "nodeAttrs": [{"target": ["tag:container"], "attr": ["funnel"]}]
    

Note on Windows Docker Desktop: If /dev/net/tun is unavailable, add TS_USERSPACE=true
to the tailscale service environment in docker-compose.yml.

--profile cloudflare — Cloudflare Tunnel

Requires:

  • Free Cloudflare account
  • A domain managed by Cloudflare DNS (can transfer existing domain for free)
  • Cloudflare Zero Trust account (free)

Step 1: Create a Cloudflare Tunnel

  1. Log in to Cloudflare Zero Trust
  2. Navigate to Networks → Tunnels
  3. Click Create a tunnel
  4. Choose Cloudflared as tunnel type
  5. Name your tunnel (e.g., obsidian-livesync)
  6. Click Save tunnel
  7. Copy the tunnel token — it looks like eyJhIjoiZX... (very long, ~400 characters)

Step 2: Configure Environment

Edit docker/.env:

CF_TUNNEL_TOKEN=eyJhIjoiZX...   # Paste the full token from Step 1
COUCHDB_DOMAIN=sync.yourdomain.com   # Must be a domain managed by Cloudflare

Step 3: Add Public Hostname Route

🚨 CRITICAL: Token-based tunnels ignore the local cloudflared.yml config file. All routing is controlled from the dashboard.

Back in the Zero Trust dashboard, in the same tunnel creation flow (or edit your tunnel later):

  1. Go to the Public Hostname tab
  2. Click Add a public hostname
  3. Configure:
    • Subdomain: sync (or your preferred subdomain)
    • Domain: Select your Cloudflare domain from dropdown
    • Type: HTTP
    • URL: couchdb:5984Do NOT use localhost!

Why couchdb:5984 not localhost:5984?

  • The cloudflared container runs inside Docker on the same network as couchdb
  • Docker's internal DNS resolves couchdb to the correct container
  • Using localhost would look inside the cloudflared container (nothing there)
  1. Under Additional application settings (expand):
    • No TLS Verify: Leave OFF (CouchDB uses plain HTTP internally, that's fine)
    • Leave other settings at defaults
  2. Click Save hostname

Step 4: Start the Stack

cd docker/
docker compose --profile cloudflare up -d

Verify containers are running:

docker ps --filter "name=livesync"

You should see:

  • livesync-couchdb — Status: Up (healthy)
  • livesync-cloudflared — Status: Up
  • livesync-init — Status: Exited (0)

Step 5: Test the Connection

# Should return 401 Unauthorized (proves CouchDB auth is working)
curl -I https://sync.yourdomain.com

# Should return {"couchdb":"Welcome",...}
curl -u admin:yourpassword https://sync.yourdomain.com

If you get 404, see Troubleshooting below.

Step 6: Configure Obsidian Plugin

In Obsidian → Settings → Self-hosted LiveSync:

Field Value
URI https://sync.yourdomain.com
Username value of COUCHDB_USER from .env
Password value of COUCHDB_PASSWORD from .env
Database name value of COUCHDB_DATABASE from .env (default: obsidiannotes)
End-to-end passphrase Choose your own — never stored server-side

Under Remote Database Configuration → Advanced:

  • Enable: Use Request API to avoid inevitable CORS problem (See "Known Issue" below for why this is critical)

🔧 Troubleshooting Cloudflare Tunnel

Problem: 404 Error / Cloud flare Generic Error Page

Diagnosis:

# Check if cloudflared is running
docker logs livesync-cloudflared --tail 20

# Look for: "Registered tunnel connection"
# If you see the connector ID, the tunnel is connected but routing is wrong

Fix: The public hostname rule is missing or incorrect.

  1. Go to Zero Trust → Networks → Tunnels → your tunnel → Edit
  2. Click Public Hostname tab
  3. Verify a hostname exists with:
    • Service Type: HTTP
    • URL: couchdb:5984 (NOT localhost:5984)
  4. If no hostname exists, add it (see Step 3 above)
  5. Wait 30 seconds for changes to propagate, then test again

Problem: Connection immediately closes / 502 Bad Gateway

Diagnosis: CouchDB is not healthy or not on the same Docker network as cloudflared.

docker ps --filter "name=livesync-couchdb"
# Status should be: Up (healthy)

docker inspect livesync-couchdb -f '{{.NetworkSettings.Networks}}'
# Should show: livesync-net

docker inspect livesync-cloudflared -f '{{.NetworkSettings.Networks}}'
# Should also show: livesync-net

Fix: If CouchDB is unhealthy, check logs:

docker logs livesync-couchdb --tail 50

Problem: 524 Timeout Errors During Sync

Root cause: Cloudflare's proxy has a 100-second idle timeout. CouchDB's replication protocol uses long-polling on the _changes feed, which can idle for longer during quiet periods.

Fix: Switch to short-polling mode in the Obsidian plugin:

  1. Obsidian → Settings → Self-hosted LiveSync
  2. Remote Database Configuration → Advanced
  3. Enable: Use Request API to avoid inevitable CORS problem
  4. Save and restart sync

This keeps all requests under 100 seconds.

Alternative: Use Tailscale or Caddy profiles instead — neither has aggressive timeouts.


Data & Backup

All vault data lives in the couchdb-data Docker named volume.

# Backup
docker run --rm -v obsidian-livesync_couchdb-data:/data \
  -v $(pwd)/backup:/backup alpine \
  tar czf /backup/couchdb-backup-$(date +%Y%m%d).tar.gz -C /data .

# Restore
docker run --rm -v obsidian-livesync_couchdb-data:/data \
  -v $(pwd)/backup:/backup alpine \
  tar xzf /backup/couchdb-backup-20260218.tar.gz -C /data

Security Notes

  • CouchDB requires authentication for all requests (configured by livesync.ini)
  • Enable End-to-End Encryption passphrase in the Obsidian plugin — vault data is
    encrypted before it ever leaves your device
  • The init container runs once and exits — it has no persistent access
  • Never expose CouchDB's admin interface (/_utils) to the public internet;
    use a firewall rule or the path-based obfuscation trick from
    self-hosted-livesync-server

Useful Commands

# View logs
docker compose logs -f couchdb
docker compose logs couchdb-init

# Re-run init (e.g. after changing credentials)
docker compose restart couchdb-init

# Stop without removing data
docker compose down

# Stop AND remove all data volumes (DESTRUCTIVE)
docker compose down -v

# Open CouchDB admin UI (Fauxton) in browser
open http://localhost:5984/_utils

Troubleshooting

Problem Solution
Init container keeps restarting CouchDB not healthy yet — wait 30s, check docker compose logs couchdb
curl: (52) Empty reply CouchDB not fully started — the healthcheck should gate this
Mobile can't connect Needs HTTPS — use tailscale or caddy profile
524 errors with Cloudflare Enable "Use Request API" toggle in Obsidian plugin
Permission denied on volumes Run docker compose down -v and retry — first-run volume ownership issue
CORS errors in browser Confirm CouchDB headers: curl -v -H "Origin: app://obsidian.md" http://localhost:5984/
CouchDB exits immediately, zero logs (Windows) Do not add :ro to the livesync.ini volume mount. CouchDB's entrypoint runs chmod 0644 on all files in /opt/couchdb/etc — read-only bind mounts cause a silent EPERM crash on Docker Desktop for Windows (WSL2). The compose file is already correct; do not modify it.
Settings in livesync.ini seem ignored Settings requiring restart (e.g. bind_address) load at start. Runtime-only settings (require_valid_user, enable_cors) are set by the init container via REST API and take effect immediately without restart.