mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-18 04:10:13 +00:00
@@ -1 +1,24 @@
|
||||
# Always checkout shell scripts with LF line endings (never CRLF)
|
||||
*.sh text eol=lf
|
||||
|
||||
# Standard text files — auto normalize on checkout
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.ini text eol=lf
|
||||
*.env text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.css text eol=lf
|
||||
|
||||
# Binary files — no line ending conversion
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.woff2 binary
|
||||
*.woff binary
|
||||
*.sh text eol=lf
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Self-hosted LiveSync — Environment Variables
|
||||
# Copy this file to .env and fill in your values.
|
||||
# NEVER commit .env to version control.
|
||||
|
||||
# =============================================================================
|
||||
# REQUIRED — CouchDB credentials
|
||||
# =============================================================================
|
||||
|
||||
# Admin username for CouchDB
|
||||
COUCHDB_USER=admin
|
||||
|
||||
# Admin password — use a strong random password (min 16 chars recommended)
|
||||
COUCHDB_PASSWORD=change_me_use_a_strong_password
|
||||
|
||||
# Name of the database the Obsidian plugin will use
|
||||
COUCHDB_DATABASE=obsidiannotes
|
||||
|
||||
# Host port CouchDB is exposed on (default: 5984)
|
||||
# For tunnel-only deployments you can set this to 127.0.0.1:5984 to block external access
|
||||
COUCHDB_PORT=5984
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: caddy (--profile caddy)
|
||||
# =============================================================================
|
||||
|
||||
# Your public domain pointing to this server (A record)
|
||||
# Example: couchdb.yourdomain.com
|
||||
COUCHDB_DOMAIN=couchdb.yourdomain.com
|
||||
|
||||
# Email for Let's Encrypt TLS certificate notifications
|
||||
ACME_EMAIL=you@yourdomain.com
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: tailscale (--profile tailscale)
|
||||
# =============================================================================
|
||||
|
||||
# Tailscale OAuth key (not a regular auth key — must be OAuth for persistent use)
|
||||
# Generate at: https://login.tailscale.com/admin/settings/oauth
|
||||
# Scopes needed: devices:write
|
||||
TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Hostname this node will have on your tailnet (becomes <hostname>.<tailnet>.ts.net)
|
||||
TS_HOSTNAME=livesync
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: cloudflare (--profile cloudflare)
|
||||
# =============================================================================
|
||||
|
||||
# Tunnel token from Cloudflare Zero Trust dashboard
|
||||
# Create at: https://one.dash.cloudflare.com/ → Networks → Tunnels → Create tunnel
|
||||
# Copy the token from the "Install connector" step
|
||||
CF_TUNNEL_TOKEN=eyJhIjoixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
@@ -0,0 +1,349 @@
|
||||
# Self-hosted LiveSync — Docker Setup
|
||||
|
||||
A fully self-hosted CouchDB stack for the [obsidian-livesync](https://github.com/vrtmrz/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](https://docs.docker.com/desktop/) (Windows/Mac/Linux) or Docker Engine + Compose plugin
|
||||
- A machine that Obsidian devices can reach over HTTPS (see profiles below)
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
cp .env.example .env
|
||||
# Edit .env — at minimum set COUCHDB_USER and a strong COUCHDB_PASSWORD
|
||||
```
|
||||
|
||||
### 3. Launch
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
### `--profile tailscale` — No Domain Required ✅ Recommended for privacy
|
||||
|
||||
**Requires**:
|
||||
- Free [Tailscale account](https://login.tailscale.com/)
|
||||
- Install the Tailscale app on all your Obsidian devices
|
||||
- Generate an **OAuth key** at: https://login.tailscale.com/admin/settings/oauth
|
||||
(Scopes: `devices:write`)
|
||||
|
||||
**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](https://login.tailscale.com/admin/acls):
|
||||
```json
|
||||
"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](https://www.cloudflare.com/)
|
||||
- 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](https://one.dash.cloudflare.com/)
|
||||
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`:
|
||||
```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:5984` ← **Do 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)
|
||||
|
||||
4. Under **Additional application settings** (expand):
|
||||
- **No TLS Verify**: Leave **OFF** (CouchDB uses plain HTTP internally, that's fine)
|
||||
- Leave other settings at defaults
|
||||
5. Click **Save hostname**
|
||||
|
||||
#### Step 4: Start the Stack
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
docker compose --profile cloudflare up -d
|
||||
```
|
||||
|
||||
Verify containers are running:
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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**:
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
# 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](https://github.com/vrtmrz/self-hosted-livesync-server)
|
||||
|
||||
---
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# 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. |
|
||||
@@ -0,0 +1,26 @@
|
||||
# Caddy config for Self-hosted LiveSync CouchDB
|
||||
# =============================================================================
|
||||
# IMPORTANT: CouchDB handles CORS itself.
|
||||
# Do NOT add CORS headers here — they will conflict with CouchDB's own headers.
|
||||
# Do NOT intercept OPTIONS requests.
|
||||
# =============================================================================
|
||||
|
||||
{
|
||||
# Email used for Let's Encrypt certificate notifications
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$COUCHDB_DOMAIN} {
|
||||
# Forward all traffic to CouchDB, preserving Host and forwarded-for headers
|
||||
reverse_proxy couchdb:5984 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output stdout
|
||||
level WARN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# cloudflared tunnel configuration for Self-hosted LiveSync
|
||||
# =============================================================================
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Create a tunnel in Cloudflare Zero Trust → Networks → Tunnels
|
||||
# 2. Copy the tunnel token to CF_TUNNEL_TOKEN in your .env
|
||||
# 3. Add a public hostname in the tunnel config:
|
||||
# Hostname : couchdb.yourdomain.com (or whatever you set COUCHDB_DOMAIN to)
|
||||
# Service : http://couchdb:5984
|
||||
#
|
||||
# Known issue: Cloudflare's 100-second proxy timeout can interrupt CouchDB's
|
||||
# long-polling replication change feed, causing 524 errors.
|
||||
# MITIGATION: In the Obsidian plugin settings, enable:
|
||||
# "Use Request API to avoid inevitable CORS problem"
|
||||
# This switches from long-poll to short-poll mode.
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
tunnel: ${CF_TUNNEL_ID}
|
||||
credentials-file: /etc/cloudflared/credentials.json
|
||||
|
||||
ingress:
|
||||
- hostname: ${COUCHDB_DOMAIN}
|
||||
service: http://couchdb:5984
|
||||
originRequest:
|
||||
# Increase timeouts for CouchDB replication streams
|
||||
connectTimeout: 30s
|
||||
keepAliveTimeout: 90s
|
||||
keepAliveConnections: 100
|
||||
noTLSVerify: false
|
||||
- service: http_status:404
|
||||
@@ -0,0 +1,30 @@
|
||||
; CouchDB local configuration for Self-hosted LiveSync
|
||||
; This file is volume-mounted into /opt/couchdb/etc/local.d/livesync.ini
|
||||
;
|
||||
; IMPORTANT: Do NOT set require_valid_user here.
|
||||
; CouchDB needs to start without auth to complete its first-run cluster setup
|
||||
; (_users, _replicator databases must be created first).
|
||||
; The couchdb-init service applies auth lockdown via REST API after first-run.
|
||||
|
||||
[couchdb]
|
||||
; Max size per document (50MB). Large enough for binary attachments.
|
||||
max_document_size = 50000000
|
||||
|
||||
[chttpd]
|
||||
; Bind on all interfaces.
|
||||
bind_address = 0.0.0.0
|
||||
port = 5984
|
||||
; 4 GB max request (handles very large vaults)
|
||||
max_http_request_size = 4294967296
|
||||
|
||||
[httpd]
|
||||
WWW-Authenticate = Basic realm="couchdb"
|
||||
|
||||
[cors]
|
||||
; These are the exact app origins Obsidian uses on desktop + mobile
|
||||
credentials = true
|
||||
origins = app://obsidian.md,capacitor://localhost,http://localhost
|
||||
|
||||
[log]
|
||||
; Reduce noise in Docker logs — set to "debug" if troubleshooting
|
||||
level = warning
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"TCP": {
|
||||
"443": {
|
||||
"HTTPS": true
|
||||
}
|
||||
},
|
||||
"Web": {
|
||||
"${TS_CERT_DOMAIN}:443": {
|
||||
"Handlers": {
|
||||
"/": {
|
||||
"Proxy": "http://127.0.0.1:5984"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowFunnel": {
|
||||
"${TS_CERT_DOMAIN}:443": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
# Self-hosted LiveSync — Docker Compose
|
||||
# =============================================================================
|
||||
# PROFILES
|
||||
# --------
|
||||
# (default) CouchDB only — LAN/localhost access, no TLS
|
||||
# Suitable for desktop-only use or testing.
|
||||
#
|
||||
# --profile caddy CouchDB + Caddy reverse proxy
|
||||
# Auto TLS via Let's Encrypt. Needs public domain + ports 80/443.
|
||||
#
|
||||
# --profile tailscale CouchDB + Tailscale sidecar
|
||||
# No domain required. HTTPS via *.ts.net PKI.
|
||||
# Needs a Tailscale account (free tier works).
|
||||
#
|
||||
# --profile cloudflare CouchDB + cloudflared tunnel daemon
|
||||
# Free public HTTPS via Cloudflare. Needs a CF account + tunnel token.
|
||||
# NOTE: Enable "Use Request API" in the Obsidian plugin to avoid 524 timeouts.
|
||||
#
|
||||
# QUICK START (local test):
|
||||
# cp .env.example .env && edit .env
|
||||
# docker compose up -d
|
||||
# curl -u admin:yourpassword http://localhost:5984/_up
|
||||
# =============================================================================
|
||||
|
||||
name: obsidian-livesync
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CouchDB — the only required service
|
||||
# ---------------------------------------------------------------------------
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
container_name: livesync-couchdb
|
||||
restart: unless-stopped
|
||||
# NOTE: Do NOT set user: here — the CouchDB entrypoint starts as root to
|
||||
# write docker.ini (from env vars), then drops to uid 5984 automatically.
|
||||
environment:
|
||||
COUCHDB_USER: ${COUCHDB_USER:?Set COUCHDB_USER in .env}
|
||||
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD:?Set COUCHDB_PASSWORD in .env}
|
||||
volumes:
|
||||
- couchdb-data:/opt/couchdb/data
|
||||
# Mount to /opt/couchdb/etc/local.ini (NOT into local.d/).
|
||||
# Do NOT use :ro — the CouchDB entrypoint runs chmod on this file at startup
|
||||
# and will crash with EPERM if the file is read-only. The file is only read
|
||||
# at startup; runtime changes go via the REST API into local.d/docker.ini.
|
||||
- ./config/livesync.ini:/opt/couchdb/etc/local.ini
|
||||
ports:
|
||||
# Exposes CouchDB on the host for LAN/localhost access.
|
||||
# The tunnel profiles (caddy/tailscale/cloudflare) provide HTTPS on top.
|
||||
# You can remove this port mapping once a tunnel profile is in use.
|
||||
- "${COUCHDB_PORT:-5984}:5984"
|
||||
healthcheck:
|
||||
# Test with admin credentials — ensures both CouchDB is up AND auth is ready.
|
||||
# ${COUCHDB_USER} / ${COUCHDB_PASSWORD} are expanded by Docker Compose here.
|
||||
test:
|
||||
- "CMD-SHELL"
|
||||
- "curl -sf -u ${COUCHDB_USER}:${COUCHDB_PASSWORD} http://localhost:5984/_session | grep -q ok || exit 1"
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 24
|
||||
start_period: 20s
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# One-shot init container — runs couchdb-init.sh after CouchDB is healthy.
|
||||
# Sets single-node cluster, auth requirements, CORS, size limits, creates DB.
|
||||
# Restarts on failure (e.g. race at first boot) but won't re-run if already done.
|
||||
# ---------------------------------------------------------------------------
|
||||
couchdb-init:
|
||||
image: curlimages/curl:latest
|
||||
container_name: livesync-init
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COUCHDB_INTERNAL_URL: http://couchdb:5984
|
||||
COUCHDB_USER: ${COUCHDB_USER}
|
||||
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
|
||||
COUCHDB_DATABASE: ${COUCHDB_DATABASE:-obsidiannotes}
|
||||
volumes:
|
||||
- ./scripts/couchdb-init.sh:/couchdb-init.sh:ro
|
||||
entrypoint: ["sh", "/couchdb-init.sh"]
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: caddy — Caddy reverse proxy with automatic Let's Encrypt TLS
|
||||
# Requirements: public domain, ports 80 + 443 open to internet
|
||||
# Usage: docker compose --profile caddy up -d
|
||||
# ---------------------------------------------------------------------------
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: livesync-caddy
|
||||
profiles: [caddy]
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COUCHDB_DOMAIN: ${COUCHDB_DOMAIN:?Set COUCHDB_DOMAIN in .env for caddy profile}
|
||||
ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL in .env for caddy profile}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: tailscale — Tailscale sidecar for mesh VPN + optional Funnel
|
||||
# Requirements: Tailscale account (free), OAuth key, Funnel enabled in ACL
|
||||
# Usage: docker compose --profile tailscale up -d
|
||||
# The CouchDB port mapping above can be removed for tailscale-only deployments.
|
||||
# ---------------------------------------------------------------------------
|
||||
tailscale:
|
||||
image: tailscale/tailscale:latest
|
||||
container_name: livesync-tailscale
|
||||
profiles: [tailscale]
|
||||
restart: unless-stopped
|
||||
hostname: ${TS_HOSTNAME:-livesync}
|
||||
environment:
|
||||
TS_AUTHKEY: ${TS_AUTHKEY:?Set TS_AUTHKEY in .env for tailscale profile}
|
||||
TS_STATE_DIR: /var/lib/tailscale
|
||||
TS_SERVE_CONFIG: /config/serve.json
|
||||
TS_USERSPACE: "false"
|
||||
TS_ACCEPT_DNS: "false"
|
||||
TS_EXTRA_ARGS: ""
|
||||
volumes:
|
||||
- tailscale-state:/var/lib/tailscale
|
||||
- ./config/ts-serve.json:/config/serve.json:ro
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
# Share CouchDB's network namespace so Tailscale can reach it on localhost
|
||||
network_mode: service:couchdb
|
||||
depends_on:
|
||||
- couchdb
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: cloudflare — cloudflared tunnel daemon
|
||||
# Requirements: Cloudflare account, tunnel token from CF Zero Trust dashboard
|
||||
# Usage: docker compose --profile cloudflare up -d
|
||||
# NOTE: Enable "Use Request API" toggle in the Obsidian LiveSync plugin settings
|
||||
# to avoid Cloudflare's 100-second proxy timeout (524 errors).
|
||||
# ---------------------------------------------------------------------------
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: livesync-cloudflared
|
||||
profiles: [cloudflare]
|
||||
restart: unless-stopped
|
||||
command: tunnel --no-autoupdate run
|
||||
environment:
|
||||
TUNNEL_TOKEN: ${CF_TUNNEL_TOKEN:?Set CF_TUNNEL_TOKEN in .env for cloudflare profile}
|
||||
volumes:
|
||||
- ./config/cloudflared.yml:/etc/cloudflared/config.yml:ro
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# =============================================================================
|
||||
# Volumes
|
||||
# =============================================================================
|
||||
volumes:
|
||||
couchdb-data:
|
||||
driver: local
|
||||
caddy-data:
|
||||
driver: local
|
||||
caddy-config:
|
||||
driver: local
|
||||
tailscale-state:
|
||||
driver: local
|
||||
|
||||
# =============================================================================
|
||||
# Networks
|
||||
# =============================================================================
|
||||
networks:
|
||||
livesync-net:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
# Self-hosted LiveSync — CouchDB Initialization Script
|
||||
# Runs once on first startup via the couchdb-init service.
|
||||
# Configures single-node cluster, auth, CORS, and size limits.
|
||||
|
||||
set -e
|
||||
|
||||
hostname="${COUCHDB_INTERNAL_URL:-http://couchdb:5984}"
|
||||
username="${COUCHDB_USER:?COUCHDB_USER is required}"
|
||||
password="${COUCHDB_PASSWORD:?COUCHDB_PASSWORD is required}"
|
||||
node="${COUCHDB_NODE:-_local}"
|
||||
|
||||
echo "==> Waiting for CouchDB at ${hostname} ..."
|
||||
# _up is publicly accessible (no auth required) — safe pre-auth wait
|
||||
until curl -sf "${hostname}/_up" 2>/dev/null | grep -q '"status":"ok"'; do
|
||||
printf '.'
|
||||
sleep 2
|
||||
done
|
||||
echo ""
|
||||
echo "==> CouchDB is up. Initializing..."
|
||||
|
||||
# 1. Enable single-node cluster
|
||||
curl -sf -X POST "${hostname}/_cluster_setup" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" \
|
||||
--user "${username}:${password}" && echo "[OK] cluster_setup"
|
||||
|
||||
# 2. Require valid user on both http interfaces
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/require_valid_user" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd/require_valid_user"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd_auth/require_valid_user" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd_auth/require_valid_user"
|
||||
|
||||
# 3. HTTP auth challenge header
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/httpd/WWW-Authenticate" \
|
||||
-H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}" && echo "[OK] httpd/WWW-Authenticate"
|
||||
|
||||
# 4. Enable CORS on both http listeners
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/httpd/enable_cors" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] httpd/enable_cors"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/enable_cors" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd/enable_cors"
|
||||
|
||||
# 5. Increase size limits for large vaults
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/max_http_request_size" \
|
||||
-H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}" && echo "[OK] chttpd/max_http_request_size"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/couchdb/max_document_size" \
|
||||
-H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}" && echo "[OK] couchdb/max_document_size"
|
||||
|
||||
# 6. CORS configuration — allow Obsidian app origins
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/cors/credentials" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] cors/credentials"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/cors/origins" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '"app://obsidian.md,capacitor://localhost,http://localhost"' \
|
||||
--user "${username}:${password}" && echo "[OK] cors/origins"
|
||||
|
||||
# 7. Create the vault database if it doesn't exist
|
||||
db="${COUCHDB_DATABASE:-obsidiannotes}"
|
||||
set +e
|
||||
status=$(curl -sf -o /dev/null -w "%{http_code}" --user "${username}:${password}" "${hostname}/${db}" 2>/dev/null)
|
||||
curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "[OK] database '${db}' already exists"
|
||||
else
|
||||
curl -sf -X PUT "${hostname}/${db}" --user "${username}:${password}" && echo "[OK] database '${db}' created" || echo "[WARN] database creation returned non-200 — may already exist"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> CouchDB initialization complete!"
|
||||
echo " URL : ${hostname}"
|
||||
echo " Database : ${db}"
|
||||
echo " Username : ${username}"
|
||||
Reference in New Issue
Block a user