Self-hosting Obsidian LiveSync with CouchDB on AWS Lightsail
I use one Obsidian vault on macOS, Linux, iPadOS and Android. I wanted local Markdown files on every device, fast synchronization, end-to-end encryption and a backend I could operate myself without exposing CouchDB directly to the internet.
This guide covers both the implementation and the boundaries around it: how a normal sync request reaches CouchDB, which credentials exist at each layer, what remains private and how recovery stays independent from synchronization. Self-hosted LiveSync is maintained by its own creator and contributors; this post documents my setup rather than providing official project guidance.
Architecture
The design separates three operational planes: normal client synchronization, administration and backup. Obsidian clients connect only through HTTPS, Caddy is the sole public application entry point, CouchDB remains private inside the Docker network and recovery does not depend on LiveSync being available.
Self-hosted LiveSync architecture, security boundaries and operational lifecycle.
Open or download the full-size editable Excalidraw SVG.
How a sync request travels
- The Self-hosted LiveSync plugin encrypts vault content on the client before transmission. Each device keeps normal local Markdown files and uses the restricted
obsidian-synccredential for the remote database. - DuckDNS resolves
vik-obsidian.duckdns.orgto the Lightsail instance's static address. The public firewall admits application traffic only on TCP 443. - Caddy terminates TLS and proxies the request to
couchdb:5984across the private Docker network. CouchDB itself is not reachable from the public internet. - CouchDB authenticates the client and limits it to the
obsidian-notesdatabase. Documents persist in thecouchdb-datavolume. - Snapshots and versioned copies of the local vault form a separate recovery path. They remain useful even if synchronization propagates an unwanted edit or deletion.
Separate planes, separate credentials
| Plane | Entry point | Credential | Purpose |
| -------------- | --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------- |
| Client sync | HTTPS on port 443 | obsidian-sync plus the LiveSync E2EE passphrase | Routine note and attachment synchronization |
| Administration | SSH on port 22 from my IP; CouchDB through loopback | Server login and obsidian-admin | Deployment, database security, maintenance and recovery |
| Backup | Lightsail snapshots and local vault history | Backup-specific access | Restore after deletion, corruption or server loss |
This separation limits the effect of a leaked client credential: it does not grant server access or CouchDB administrative privileges. The E2EE passphrase is also distinct from both CouchDB passwords, so database authentication and payload encryption remain independent controls.
1. Set the security boundaries
I created a small Ubuntu instance in the London AWS region, attached a static IPv4 address and pointed DuckDNS at it. The first boundary is the Lightsail firewall: normal clients get one public path, while administrative access has a narrower source restriction.
- SSH on port 22 is limited to my current public IP address.
- HTTPS on port 443 is public.
- HTTP on port 80 is closed.
- CouchDB on port 5984 has no public rule.
On the server I installed Docker, enabled automatic security updates and added a small swapfile:
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y docker.io docker-compose-v2 curl jq unattended-upgrades
sudo systemctl enable --now docker
sudo dpkg-reconfigure -plow unattended-upgrades
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
Docker group membership is effectively root access. I either use Docker through sudo or grant it only to the administrative account.
2. Run the official LiveSync stack
I use the Docker files published by Self-hosted LiveSync because the stack includes an initializer for CouchDB. For reproducibility, the URLs should point to a reviewed commit SHA rather than a moving main branch.
sudo mkdir -p /opt/obsidian-couchdb/{config,scripts}
sudo chown -R "$USER":"$USER" /opt/obsidian-couchdb
cd /opt/obsidian-couchdb
curl -fsSL https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/docker/docker-compose.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/docker/config/livesync.ini -o config/livesync.ini
curl -fsSL https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/docker/scripts/couchdb-init.sh -o scripts/couchdb-init.sh
chmod 755 scripts/couchdb-init.sh
My environment file keeps CouchDB bound to the host loopback interface. The real passwords and email address live in a password manager and never enter source control.
COUCHDB_USER=obsidian-admin
COUCHDB_PASSWORD=<STRONG_RANDOM_ADMIN_PASSWORD>
COUCHDB_DATABASE=obsidian-notes
COUCHDB_PORT=127.0.0.1:5984
COUCHDB_DOMAIN=vik-obsidian.duckdns.org
ACME_EMAIL=<MY_REAL_EMAIL>
TS_AUTHKEY=unused
CF_TUNNEL_TOKEN=unused
chmod 600 .env
docker compose config -q
docker compose up -d
curl -u obsidian-admin http://127.0.0.1:5984/_up
The final command should return a CouchDB status of ok. I also confirm that port 5984 listens on 127.0.0.1 rather than every interface. CouchDB stores its state in the named couchdb-data volume; the one-shot initializer waits for a healthy database, provisions the required settings and system databases, then exits successfully.
3. Put Caddy in front of CouchDB
DuckDNS points my hostname at the Lightsail static address. Caddy terminates TLS on port 443 and forwards requests to CouchDB over the private Docker network. I removed the port 80 mapping from the Caddy service and kept only 443. TLS-ALPN-01 validation allows Caddy to obtain and renew the certificate over port 443, so the design does not need to expose port 80 for an HTTP challenge.
{
email {$ACME_EMAIL}
auto_https disable_redirects
}
{$COUCHDB_DOMAIN} {
tls {
issuer acme {
disable_http_challenge
}
}
reverse_proxy couchdb:5984
}
docker compose --profile caddy up -d
curl -I https://vik-obsidian.duckdns.org
curl -u obsidian-admin https://vik-obsidian.duckdns.org/_up
An unauthenticated request should receive 401. An authenticated health request should succeed. From another machine I also verify that ports 80 and 5984 remain closed.
4. Separate administration from synchronization
The administrator account is reserved for setup, maintenance and recovery. It is never stored in a normal Obsidian client. Every client instead uses a restricted account named obsidian-sync that is a member of only the vault database.
SYNC_PASSWORD="$(openssl rand -hex 32)"
curl -u "$COUCHDB_USER:$COUCHDB_PASSWORD" \
-X PUT http://127.0.0.1:5984/_users/org.couchdb.user:obsidian-sync \
-H 'Content-Type: application/json' \
-d "{\"name\":\"obsidian-sync\",\"password\":\"$SYNC_PASSWORD\",\"roles\":[],\"type\":\"user\"}"
curl -u "$COUCHDB_USER:$COUCHDB_PASSWORD" \
-X PUT http://127.0.0.1:5984/obsidian-notes/_security \
-H 'Content-Type: application/json' \
-d '{"admins":{"names":[],"roles":[]},"members":{"names":["obsidian-sync"],"roles":[]}}'
I test this credential through the public HTTPS endpoint before adding it to Obsidian.
5. Configure Obsidian one device at a time
My Mac was the primary vault, so I made a full backup before enabling LiveSync. In the plugin I entered the HTTPS hostname, database name obsidian-notes and the restricted obsidian-sync credential.
I enabled LiveSync end-to-end encryption with a separate passphrase. Encryption happens before the payload leaves the device; TLS then protects that encrypted payload and the Basic Auth credential in transit. The CouchDB admin password, client password, encryption passphrase and Setup URI passphrase are all different.
After the first synchronization completed, I generated a protected Setup URI. I onboarded Linux, iPadOS and Android individually, testing note edits and attachments in both directions before adding the next device. I do not run another filesystem synchronization service against the same vault.
6. Treat synchronization and backup separately
A synchronized deletion can reach every device, so LiveSync is not a backup. I use Lightsail snapshots for infrastructure recovery, a recoverable copy of the couchdb-data volume and a separate versioned copy of the local Markdown vault for note history. A backup is useful only after a restore test, so recovery verification is part of the update routine rather than an emergency-only exercise.
These commands cover the checks I use most often:
docker compose --profile caddy ps -a
docker compose logs couchdb --tail=100
docker compose logs caddy --tail=100
ss -ltn | grep 5984
df -h
docker system df
Before updating the stack I back up CouchDB and the vault, review the LiveSync release notes, record the current versions, validate the Compose configuration and test synchronization again after deployment.
Result
The finished service gives every device a normal local Obsidian vault while CouchDB handles synchronization behind authenticated HTTPS. The architecture makes the responsibilities visible: Caddy owns the public edge, CouchDB owns synchronized state, the plugin owns client-side encryption and independent backups own recovery. Only port 443 is public for application traffic, clients never receive the administrator credential and losing the sync server does not mean losing the vault.