Skip to content

Repository files navigation

Obsidian Self-hosted LiveSync for Coolify

Production-oriented CouchDB deployment for Obsidian Self-hosted LiveSync, designed for deployment as a Docker Compose resource in Coolify.

The stack keeps CouchDB behind Coolify's HTTPS reverse proxy, provisions the required CouchDB settings automatically, and gives each person their own vault database reachable only by their own account — never the CouchDB server administrator credentials.

Adding someone is a name in Coolify's LIVESYNC_USERS, one line in docker-compose.yml, and a redeploy.

Features

  • One vault database per person, created automatically from a Coolify-managed roster — adding someone is a name, a password line, and a redeploy.
  • Each person gets a CouchDB account with database-admin rights on their own vault and nothing else. CI asserts the refusals, not just the access.
  • One-shot, idempotent provisioning: re-running against an already-configured server is expected and safe, and CI proves it by running it twice.
  • CouchDB stays behind Coolify's HTTPS proxy via SERVICE_FQDN_COUCHDB_5984, with no host port mapping and a health check.
  • Single-node setup, authentication, and LiveSync-compatible CORS applied automatically; passwords generated by Coolify.
  • Base images pinned to exact versions in Dockerfile, with Renovate proposing updates.
  • The provisioning script has no remote dependencies, and is type-checked, linted, and unit-tested in CI.
  • Every run scans for committed secrets and scans both built images for known CVEs.

Repository layout

.
├── docker-compose.yml           services, and each person's password line
├── Dockerfile                   both image targets and the pinned base images
├── config/livesync.ini          static CouchDB config, applied before provisioning
├── scripts/
│   ├── couchdb-init.ts          the provisioning service
│   ├── couchdb-init.test.ts     its unit tests
│   ├── deno.sh                  runs deno, or the pinned image if deno is absent
│   └── validate.sh              everything CI runs, runnable locally
├── .pre-commit-config.yaml      the commit and push hooks
└── .github/workflows/ci.yml     the pipeline, which mostly calls validate.sh

Root dotfiles and renovate.json configure formatting, linting, secret scanning, ignore rules, and dependency updates.

Architecture

The Compose application contains two services:

couchdb

Runs CouchDB and stores persistent data in the couchdb-data named volume. Only internal port 5984 is exposed to the Docker network. Coolify publishes the service through its HTTPS reverse proxy.

couchdb-init

A one-shot Deno service that waits for CouchDB to become healthy and then, once for the server:

  1. verifies or enables single-node mode;
  2. enables authenticated access; and
  3. applies the LiveSync CORS settings and request-size limits.

Then, for each person in the roster:

  1. creates their vault database if needed;
  2. initializes or verifies its LiveSync database version;
  3. creates or updates their CouchDB account;
  4. grants that account database-admin rights on their database only; and
  5. verifies that the account can reach it.

The whole roster is validated before any of this begins, so a typo in the last entry does not leave the first few people half-provisioned. A failure on one person aborts the run: provisioning is idempotent, so the bounded restart finishes what a transient error interrupted, and a half-configured deployment must not report success.

A successful couchdb-init container exits with status code 0. It is expected to be stopped after provisioning completes.

Deploy with Coolify

1. Create the resource

Create a new Docker Compose resource in Coolify and point it at this Git repository. Use the repository root as the build context and docker-compose.yml as the Compose file.

The Compose file contains Coolify's magic FQDN variable for CouchDB port 5984:

SERVICE_FQDN_COUCHDB_5984

Assign the HTTPS domain for the couchdb service in Coolify. Do not add a separate host port mapping for port 5984.

2. Review environment variables

The roster is a Coolify variable, editable in the environment-variable view. Each person's password line stays in docker-compose.yml, on the couchdb-init service. The stack ships with one person:

Variable Default Purpose
LIVESYNC_USERS livesync Comma-separated roster
COUCHDB_ADMIN_USER admin CouchDB server administrator name
      - LIVESYNC_PASSWORD_LIVESYNC=${SERVICE_PASSWORD_64_LIVESYNC}

LIVESYNC_USERS is the roster. Each name gets its own vault database and its own password variable, both derived from the name:

Name Database Password variable
livesync vault-livesync LIVESYNC_PASSWORD_LIVESYNC
alice vault-alice LIVESYNC_PASSWORD_ALICE
alice.smith vault-alice-smith LIVESYNC_PASSWORD_ALICE_SMITH

Names may contain letters, digits, dots, underscores, and hyphens. Two names that differ only by case, dots, or underscores are rejected at startup rather than silently sharing one vault.

Coolify generates any SERVICE_PASSWORD_64_* variable the Compose file references, so a new person's password is created for you:

Variable Purpose
SERVICE_PASSWORD_64_COUCHDBADMIN CouchDB server administrator password
SERVICE_PASSWORD_64_LIVESYNC Password for the livesync person

Prefer single-word names. Coolify only generates a SERVICE_PASSWORD_* value when the whole variable name has three underscores or fewer, so SERVICE_PASSWORD_64_ALICE is generated but SERVICE_PASSWORD_64_ALICE_SMITH is silently left empty. A person whose name contains a dot or underscore needs their password set by hand in Coolify. This is why the administrator's variable is COUCHDBADMIN and not COUCHDB_ADMIN.

One optional variable applies server-wide:

Variable Purpose
CORS_ORIGINS Allowed LiveSync application origins. Defaults to the Obsidian desktop and mobile origins — see DEFAULT_ORIGINS in scripts/couchdb-init.ts.

3. Deploy

Deploy the Compose resource. Wait until couchdb is healthy and inspect the couchdb-init logs.

A completed initialization ends with messages equivalent to:

CouchDB provisioning completed successfully.
Configure each Obsidian client with its own username and database:
  livesync -> vault-livesync

The couchdb-init service should then be in an exited/success state. The couchdb service remains running.

Managing people

Adding a person

Edit two places and redeploy. First, the roster, in Coolify's environment-variable view:

LIVESYNC_USERS=livesync,alice

Second, their password line in docker-compose.yml:

      - LIVESYNC_PASSWORD_LIVESYNC=${SERVICE_PASSWORD_64_LIVESYNC}
      - LIVESYNC_PASSWORD_ALICE=${SERVICE_PASSWORD_64_ALICE}

Coolify generates SERVICE_PASSWORD_64_ALICE on the next deploy. Provisioning creates vault-alice, grants alice database-admin on it alone, and leaves every existing vault untouched — re-running against an already-provisioned server is expected and safe.

Read alice's generated password from Coolify's environment-variable view to configure her Obsidian client.

A name and its password line must be added together. If you forget the password line, the deployment fails with LIVESYNC_PASSWORD_ALICE is required for LIVESYNC_USERS entry 'alice' — and reports every other roster problem in the same message, so several people can be added in one pass.

Removing a person

Delete their name from LIVESYNC_USERS in Coolify, delete their password line from docker-compose.yml, and redeploy. Their account and vault database are not deleted — provisioning never destroys data, because a typo in the roster would otherwise be unrecoverable.

To actually remove them, delete the CouchDB user document and their vault-<name> database by hand, after taking a backup.

Rotating someone's password

Change the value Coolify holds for their SERVICE_PASSWORD_64_<NAME> and redeploy. Provisioning detects that the stored password no longer matches and resets the account, leaving their vault contents untouched. They then reconnect their Obsidian client with the new password.

Configure Obsidian Self-hosted LiveSync

Install and enable the Self-hosted LiveSync community plugin in Obsidian.

Each person uses their own connection values:

LiveSync setting Value
URI The HTTPS URL assigned to the CouchDB service by Coolify (the same for everyone)
Username Their name from LIVESYNC_USERS, for example alice
Password Coolify value of their SERVICE_PASSWORD_64_<NAME>
Database vault-<name>, for example vault-alice

The couchdb-init logs print the name-to-database mapping for every person, so read it there rather than deriving it by hand.

Use the normal CouchDB connection mode first. The CORS configuration already allows the standard desktop and mobile Obsidian origins.

End-to-end encryption

Enable Self-hosted LiveSync end-to-end encryption and use a strong passphrase that is different from every CouchDB password.

The LiveSync encryption passphrase protects vault content independently from CouchDB authentication. Do not store that passphrase in this repository.

Each person chooses their own passphrase in their own Obsidian client. The server neither sets nor knows it — this stack separates people by access control, not by cryptography. A CouchDB server administrator can read any vault whose owner did not enable end-to-end encryption. If that matters for your deployment, have everyone turn it on.

Security model

Network exposure

The Compose file uses:

expose:
  - "5984"

It intentionally does not use:

ports:
  - "5984:5984"

External traffic should therefore reach CouchDB only through the Coolify proxy and its HTTPS endpoint.

Administrator account

The CouchDB admin account is a server administrator used by the provisioning service. Do not configure Obsidian clients with this account.

Per-person accounts

Each person's vault-<name> database gets a security object naming only them, in both admins.names and members.names. Nobody else — including other people on this same server — can read or write it.

Populating members is what scopes the grant. An empty members block means "every authenticated user" in CouchDB, so a security object listing only admins would leave the vault readable and writable by every account in _users. scripts/couchdb-init.ts never writes one, and scripts/validate.sh proves the result by provisioning two people and asserting all four cross-access outcomes:

  ok   alice -> vault-alice : 200
  ok   alice -> vault-bob   : 403
  ok   bob   -> vault-bob   : 200
  ok   bob   -> vault-alice : 403

Database admin, rather than plain member, is required: LiveSync builds indexes through pouchdb-find, and CouchDB refuses design documents to members with "You are not a db or server admin." A member-only account connects and syncs briefly, then fails when it builds an index.

A CouchDB database administrator is still confined to that one database. It is not a server administrator: it cannot list databases, create or delete them, read _users, or modify server-wide configuration.

CORS

The allowed origins default to the normal Obsidian desktop and mobile application origins, declared as DEFAULT_ORIGINS in scripts/couchdb-init.ts and overridable with CORS_ORIGINS.

Do not replace them with * when credentials are enabled: CouchDB is configured with credentials = true, and the two together would let any web page authenticate against the vault with a visitor's stored credentials.

Persistence and backups

Only /opt/couchdb/data is persisted:

couchdb-data:/opt/couchdb/data

CouchDB configuration and the provisioning script live in the built images. This avoids a persistent configuration volume masking updated image configuration after a redeploy.

Back up the couchdb-data volume using the backup mechanism available on the Coolify host. Treat the remote CouchDB database as synchronization infrastructure, not as the only backup of the vault.

Before a major CouchDB or LiveSync upgrade, create a fresh backup of both the vault and the CouchDB data volume.

Updating

The CouchDB and Deno base images are pinned to exact versions in the FROM lines of Dockerfile. Renovate proposes updates for both, and for the GitHub Actions and scanner images used by CI; review release notes before merging a CouchDB major-version upgrade.

Renovate is configured in renovate.json, which extends a shared preset rather than restating policy here — so the update schedule, grouping, and commit style are whatever that preset defines.

The provisioning script has no remote dependencies. The two values it needs from upstream Self-hosted LiveSync — the remote schema version and the version-marker document id — are declared as constants at the top of scripts/couchdb-init.ts, mirroring VER and VERSIONING_DOCID in @vrtmrz/livesync-commonlib.

Importing that library instead would pull its entire dependency tree — an AWS S3 client, a peer-to-peer transport, the PouchDB packages — to supply two constants and two HTTP requests the script already makes. The trade is a manual pin in exchange for no supply-chain surface. Bump both constants together when tracking a new LiveSync release.

scripts/validate.sh runs Deno with --network none, so adding an import to the provisioning script fails validation rather than silently reintroducing that surface.

Validation

Run the repository validation script on a machine with Docker and Docker Compose available:

./scripts/validate.sh

It performs the following checks:

  • fails if any version-controlled file is empty;
  • runs hadolint, shellcheck, actionlint, and yamllint, skipping any that is not installed;
  • type-checks, lints, format-checks, and unit-tests scripts/couchdb-init.ts — using the pinned Deno image with --network none when Deno is not installed locally;
  • validates the resolved Docker Compose configuration;
  • builds the CouchDB and Deno Docker targets;
  • runs provisioning twice against a live CouchDB with a two-person roster and requires both runs to succeed, which is what proves idempotence;
  • asserts the full isolation matrix — each person reaches their own vault (200), is refused on the other's (403), and an unrelated authenticated account is refused on both; and
  • runs git diff --check when executed inside a Git work tree.

GitHub Actions runs the same validation for pushes to main, pull requests, and manual workflow runs, and adds two CI-only steps: a gitleaks secret scan and a trivy scan of both built images for HIGH and CRITICAL CVEs.

Hooks

.pre-commit-config.yaml wires the same checks into Git, so they run before a mistake reaches CI. Install them once per clone with prek (or pre-commit, which reads the same file):

prek install

The hooks are split by how long they take, because a gate that is slow enough to be skipped is not a gate:

  • on commit, a second or two: file hygiene, shellcheck, shfmt, hadolint, actionlint, zizmor, yamllint, markdownlint, a staged-changes gitleaks scan, trivy config, JSON Schema validation of the workflow and Renovate files, and deno fmt/lint/check;
  • on push, minutes: opengrep, the Deno unit tests, and ./scripts/validate.sh in full — Docker builds and the CouchDB integration run included.

Every hook that upstream publishes comes from that project's own repository, so renovate bumps the versions here alongside everything else. Four have no upstream hook and run from PATH or through scripts/deno.sh: Deno, trivy, opengrep, and validate.sh itself.

Run everything without committing:

prek run --all-files                     # the commit-stage hooks
prek run --all-files --hook-stage pre-push   # those plus the slow ones

Troubleshooting

couchdb-init fails

Provisioning failures are fatal by design, so that Coolify does not silently report a partially configured deployment as ready. The service retries a bounded number of times — set by the restart: policy on couchdb-init in docker-compose.yml — and then stops. A container still cycling long after that is a bug, not a slow start.

Inspect its logs. Common causes include an invalid database name, a missing generated password, or CouchDB being unable to initialize its persistent data directory. The readiness timeout reports the last failure it saw, so a persistent HTTP 401 or a DNS error is named rather than hidden behind "did not become ready in time".

Obsidian reports a CORS or fetch error

Verify that the external URI is the HTTPS URL published by Coolify and that the request is routed to CouchDB port 5984. Do not add the public domain to CORS_ORIGINS merely because it is the server address; the browser origin is the Obsidian application origin, which is already included by default.

If custom HTTP headers are configured in LiveSync, add those header names to CORS_HEADERS in scripts/couchdb-init.ts. That constant is the only place CORS is configured: CouchDB loads local.d/docker.ini after local.ini, so a copy in config/livesync.ini would be silently overridden and would leave this repository documenting an allow-list the server does not enforce.

Authentication succeeds but the database cannot be opened

A 403 here almost always means the client is pointed at someone else's vault. Confirm the database name is that person's own vault-<name> — the couchdb-init logs print the mapping — and that the client uses their own password, not the CouchDB administrator password and not another person's.

LIVESYNC_USER is no longer supported

The stack moved from a single shared account to one vault per person. LIVESYNC_USER, LIVESYNC_PASSWORD, and COUCHDB_DATABASE were removed, and setting any of them is a startup error rather than a silent no-op — ignoring COUCHDB_DATABASE would provision an empty vault while leaving the intended one untouched.

Replace them with a roster: see Adding a person.

Fauxton

CouchDB's Fauxton interface is served by the same CouchDB HTTP endpoint under /_utils/; there is no separate Fauxton port. Authentication remains required after provisioning.

License

MIT. See LICENSE.

About

Production-oriented CouchDB deployment for Obsidian Self-hosted LiveSync on Coolify. One vault database per person, provisioned automatically and isolated by CouchDB access control.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages