Obsidian Sync Server add-ons (CouchDB LiveSync backend) - three flavours (#2965)

* full commit obsidian

* updates on readme and clarification for addons community for easy tag on
issues

* becouse of https://github.com/alexbelgium/hassio-addons/issues/2966

* restore build.yaml for obsidian addons

---------

Co-authored-by: ToledoEM <8144940+ToledoEM@users.noreply.github.com>
This commit is contained in:
Enrique
2026-08-12 16:21:14 +01:00
committed by GitHub
parent 8d04564f94
commit dbd74e0391
49 changed files with 1796 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
{
"aurral": "petruknw",
"bentopdf": "ToledoEM",
"cleanuparr": "PierreNa",
"gitea": "baldarn",
"kometa": "akrigator",
@@ -8,7 +9,10 @@
"manyfold": "ToledoEM",
"navidrome": "baldarn",
"netalertx": "jokob-sk",
"nginx proxy manager": "ToledoEM",
"nginx_webserver_proxy": "ToledoEM",
"obsidian_syncserver_npm": "ToledoEM",
"obsidian_syncserver_solo": "ToledoEM",
"obsidian_syncserver_ssl": "ToledoEM",
"openproject": "baldarn",
"resiliosync": "tyjtyj",
"spotweb": "woutercoppens",

View File

@@ -8,6 +8,7 @@ When an issue mentions one of these add-ons (in the title or body), a GitHub Act
| Add-on folder | Initial submitter |
| --- | --- |
| `bentopdf` | [@ToledoEM](https://github.com/ToledoEM) |
| `emby` | [@petersendev](https://github.com/petersendev) |
| `gitea` | [@baldarn](https://github.com/baldarn) |
| `kometa` | [@akrigator](https://github.com/akrigator) |
@@ -15,6 +16,10 @@ When an issue mentions one of these add-ons (in the title or body), a GitHub Act
| `manyfold` | [@ToledoEM](https://github.com/ToledoEM) |
| `navidrome` | [@baldarn](https://github.com/baldarn) |
| `netalertx` | [@jokob-sk](https://github.com/jokob-sk) |
| `nginx_webserver_proxy` | [@ToledoEM](https://github.com/ToledoEM) |
| `obsidian_syncserver_npm` | [@ToledoEM](https://github.com/ToledoEM) |
| `obsidian_syncserver_solo` | [@ToledoEM](https://github.com/ToledoEM) |
| `obsidian_syncserver_ssl` | [@ToledoEM](https://github.com/ToledoEM) |
| `openproject` | [@baldarn](https://github.com/baldarn) |
| `resiliosync` | [@tyjtyj](https://github.com/tyjtyj) |
| `spotweb` | [@woutercoppens](https://github.com/woutercoppens) |

View File

@@ -0,0 +1,17 @@
# Changelog
## 3.5.2.1
- Pin the Nginx Proxy Manager base image to 2.15.1 instead of tracking :latest, so builds are reproducible and the CouchDB runtime copied in from couchdb:3.5.2 keeps a known-compatible Debian trixie ABI
## 3.5.2
- Initial release: couchdb:3.5.2 as an Obsidian Self-hosted LiveSync backend, bundled with Nginx Proxy Manager for TLS and certificate management
- NPM admin UI on port 81; HTTPS on 443; CouchDB also reachable directly on 5984
- Seeds a default nginx host proxying to CouchDB with the settings LiveSync needs: Authorization header passthrough and WebSocket upgrade
- Applies the CouchDB configuration LiveSync requires on every start: single-node cluster, CORS for Obsidian app origins, mandatory authentication, 4 GB max request size, 50 MB max document size
- Creates the vault database automatically
- Generates and persists a strong admin password when none is set
- Stores data under /config/obsidian-syncserver/data so it survives reinstalls and is included in Home Assistant backups
- Symlinks /etc/letsencrypt to /data so NPM certificates persist across restarts

View File

@@ -0,0 +1,95 @@
# Obsidian Sync Server NPM
CouchDB set up as a backend for the [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) plugin in Obsidian, with Nginx Proxy Manager bundled in for TLS and certificate handling.
## How it fits together
Both run in one container under s6 supervision. CouchDB listens on 5984 and holds the vault. Nginx Proxy Manager listens on 80, 81 and 443, and proxies HTTPS through to CouchDB.
On first start the add-on seeds a default nginx host, so port 80 reaches CouchDB before you have configured anything. Create a real proxy host in the admin UI and that takes over.
The seeded config includes the two things LiveSync depends on. `proxy_pass_request_headers on` keeps the `Authorization` header intact, which matters because CouchDB authenticates every request. The `Upgrade` and `Connection` headers allow the long-lived connections replication needs.
Build your own proxy host in the UI and you have to switch Websockets Support on for the same reason.
## Ports
| Port | Use |
| :--- | :--- |
| 443 | HTTPS, point Obsidian here |
| 81 | NPM admin UI |
| 80 | HTTP, certificate validation and redirect |
| 5984 | CouchDB directly |
Since this add-on binds 80, 81 and 443, it cannot run alongside the Nginx Proxy Manager + Static Web Server add-on or anything else holding those ports.
## First login
The admin UI on port 81 starts with well-known default credentials: `admin@example.com` and `changeme`. NPM forces a change on first login. Do it before this add-on is reachable by anything you do not control.
## Certificates
NPM keeps certificates in `/etc/letsencrypt`, which this add-on symlinks to `/data/letsencrypt` so they survive restarts and reinstalls.
For a domain that does not resolve publicly, use a DNS Challenge when requesting a Let's Encrypt certificate. HTTP validation needs the domain to reach port 80 from the internet.
## What the add-on configures in CouchDB
A stock CouchDB will not work as a LiveSync backend. On every start this add-on applies the settings the plugin needs, matching what upstream's own provisioning tool does:
| Setting | Value | Why |
| :--- | :--- | :--- |
| `chttpd/require_valid_user` | `true` | No anonymous access |
| `chttpd_auth/require_valid_user` | `true` | No anonymous access to the auth endpoints |
| `httpd/WWW-Authenticate` | `Basic realm="couchdb"` | Prompts for credentials |
| `httpd/enable_cors`, `chttpd/enable_cors` | `true` | Obsidian behaves like a browser client |
| `cors/credentials` | `true` | Lets it send the auth header cross-origin |
| `cors/origins` | `app://obsidian.md,capacitor://localhost,http://localhost` | Desktop and mobile app origins |
| `chttpd/max_http_request_size` | `4294967296` | Large vault batches |
| `couchdb/max_document_size` | `50000000` | Large notes and attachments |
These get re-applied on each start, so editing them by hand in Fauxton will not stick.
## Storage
The vault database lives in `/config/obsidian-syncserver/data` rather than the add-on's `/data` directory, so it survives a reinstall and **gets picked up by Home Assistant backups**. NPM's own database and certificates live in `/data`.
If you did not set a CouchDB password, the generated one is in `/config/obsidian-syncserver/admin_password`.
## Troubleshooting
Check CouchDB directly first. It separates a CouchDB problem from a proxy problem in one command:
```bash
curl -u admin:YOURPASSWORD http://homeassistant.local:5984/obsidian
```
If that works, CouchDB is fine and whatever is failing lives in the proxy layer.
If the add-on will not start, look for `ERROR` in the log. A malformed `database` name or an unwritable `/config` both stop startup with a message saying which.
If sync connects and then stalls, WebSocket upgrade is off. Turn on Websockets Support in the proxy host settings.
If everything returns 401 through the proxy but works on 5984, the proxy host is not passing the `Authorization` header through.
If desktop syncs but mobile does not, the certificate is either untrusted by the phone or issued for a different hostname. Check with:
```bash
openssl s_client -connect your-domain:443 </dev/null | openssl x509 -noout -subject -dates
```
If the add-on will not start because of a port conflict, something else holds 80, 81 or 443. Stop it, or switch to the plain Obsidian Sync Server behind the proxy you already have.
To see the applied CouchDB configuration:
```bash
curl -u admin:YOURPASSWORD http://homeassistant.local:5984/_node/_local/_config/cors
```
## Updates
This add-on tracks two upstream projects, CouchDB and Nginx Proxy Manager, and the repository's updater handles one upstream per add-on. Its version gets bumped by hand rather than by the weekly update workflow.
## Backups
Home Assistant backs up `/config`, which covers the vault database. For a copy you can move elsewhere, use CouchDB replication or export from Fauxton at `http://<host>:5984/_utils`.

View File

@@ -0,0 +1,48 @@
# Global build args must be declared before the first FROM to be usable there.
ARG BUILD_FROM=jc21/nginx-proxy-manager:2.15.1
ARG COUCHDB_FROM=couchdb:3.5.2
# Stage 1: source of the CouchDB runtime. CouchDB ships a self-contained
# release under /opt/couchdb, including its own Erlang runtime (erts-*), so
# it can be lifted into another image without installing Erlang separately.
FROM ${COUCHDB_FROM} AS couchdb
# Stage 2: Nginx Proxy Manager, which supplies s6-overlay and the admin UI.
# Both images are Debian trixie, so the CouchDB release is binary compatible.
FROM ${BUILD_FROM}
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
jq \
openssl \
ripgrep \
libicu76 \
libmozjs-128-0 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=couchdb /opt/couchdb /opt/couchdb
COPY --from=couchdb /usr/local/bin/docker-entrypoint.sh /docker-entrypoint.sh
# CouchDB refuses to run as root and expects to own its runtime directories.
RUN groupadd -g 5984 couchdb \
&& useradd -u 5984 -g couchdb -d /opt/couchdb -s /bin/bash couchdb \
&& chown -R couchdb:couchdb /opt/couchdb \
&& chmod +x /docker-entrypoint.sh
COPY run.sh /run.sh
COPY rootfs /
RUN chmod +x /run.sh \
&& chmod +x /etc/s6-overlay/s6-rc.d/couchdb/run \
&& chmod +x /etc/s6-overlay/s6-rc.d/couchdb/finish \
&& chmod +x /etc/s6-overlay/s6-rc.d/addon-init/script
ARG BUILD_VERSION
LABEL \
io.hass.version="${BUILD_VERSION}" \
io.hass.type="addon" \
io.hass.arch="aarch64|amd64"
ENTRYPOINT ["/init"]

View File

@@ -0,0 +1,101 @@
# Home assistant add-on: Obsidian Sync Server NPM
![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_npm%2Fconfig.yaml)
![Ingress](https://img.shields.io/badge/dynamic/yaml?label=Ingress&query=%24.ingress&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_npm%2Fconfig.yaml)
![Arch](https://img.shields.io/badge/dynamic/yaml?color=success&label=Arch&query=%24.arch&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_npm%2Fconfig.yaml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9c6cf10bdbba45ecb202d7f579b5be0e)](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
[![GitHub Super-Linter](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/weekly-supelinter.yaml?label=Lint%20code%20base)](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
[![Builder](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/onpush_builder.yaml?label=Builder)](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
Runs CouchDB as a sync backend for the [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) plugin in Obsidian, with [Nginx Proxy Manager](https://nginxproxymanager.com/) bundled in to handle TLS.
This add-on is only the server side. Install the companion plugin in Obsidian: <https://community.obsidian.md/plugins/obsidian-livesync>
> [!IMPORTANT]
>
> - Before installing or upgrading this add-on or the obsidian livesync plugin, please back up your vault.
> - Not compatible with other synchronisation solution (including iCloud and Obsidian Sync).
> - For backups, use [Differential ZIP Backup](https://github.com/vrtmrz/diffzip).
Your vault syncs between your own devices through Home Assistant. No Obsidian Sync subscription, and the notes stay on your network.
This version has everything mobile Obsidian needs in one add-on. NPM requests and renews the certificates and proxies HTTPS through to CouchDB. Pick it if you do not already run a reverse proxy.
| Add-on | TLS | Use when |
| :--- | :--- | :--- |
| [Obsidian Sync Server](../obsidian_syncserver_solo/README.md) | none | You already run a reverse proxy |
| [Obsidian Sync Server SSL](../obsidian_syncserver_ssl/README.md) | CouchDB serves HTTPS from your certificates in `/ssl` | You have certificates on the Home Assistant machine |
| Obsidian Sync Server NPM (this one) | Bundled Nginx Proxy Manager | You have no proxy and want certificate handling included |
Note that this add-on binds ports 80, 81 and 443. If you already run the Nginx Proxy Manager + Static Web Server add-on, or anything else on those ports, only one of them can be running at a time. In that case use the plain [Obsidian Sync Server](../obsidian_syncserver_solo/README.md) and add a proxy host to the NPM you already have.
## Ports
| Port | Use |
| :--- | :--- |
| 443 | HTTPS, point Obsidian here |
| 81 | Nginx Proxy Manager admin UI |
| 80 | HTTP, certificate validation and redirect |
| 5984 | CouchDB directly, for desktop or local tools |
## Installation
1. Add the repository `https://github.com/alexbelgium/hassio-addons` to Home Assistant, then install the add-on.
2. Set a password under Configuration. Leaving it blank generates one and prints it in the log on first start.
3. Start the add-on and look for `Ready.` in the log.
4. Open the NPM admin UI on port 81. The default login is `admin@example.com` with password `changeme`, and NPM makes you change both on first login. Do that now rather than later.
## Getting a real certificate
Port 443 answers out of the box, but with a self-signed certificate that mobile Obsidian will reject. To fix that:
1. In the NPM admin UI, go to SSL Certificates, then Add SSL Certificate, then Let's Encrypt.
2. Enter the domain name pointing at your Home Assistant machine, plus your email.
3. If the domain has no public IP, tick Use a DNS Challenge and pick your DNS provider.
4. Once the certificate is issued, go to Hosts, then Proxy Hosts, then Add Proxy Host:
- Domain Names: your domain
- Scheme: `http`
- Forward Hostname / IP: `127.0.0.1`
- Forward Port: `5984`
- Websockets Support: on. LiveSync will not sync without it.
- On the SSL tab, select your certificate and turn on Force SSL.
## Configuration
```yaml
username: admin
password: ""
database: obsidian
log_level: info
```
`username` and `password` are the CouchDB administrator credentials that the LiveSync plugin uses, separate from the NPM admin login. A blank password gets generated on first start and saved to `/config/obsidian-syncserver/admin_password`.
`database` is the CouchDB database holding your vault. The add-on creates it if it does not exist.
`log_level` sets CouchDB log verbosity.
## Connecting Obsidian
Install Self-hosted LiveSync from Obsidian's community plugins. In its settings, pick the manual setup and fill in:
- URI: `https://your-domain`
- Username and password: the CouchDB credentials above
- Database name: `obsidian`, unless you changed it
Hit Test Database Connection to check it, then turn on end-to-end encryption with a passphrase. With that on, the server only ever holds ciphertext.
[DOCS.md](DOCS.md) covers troubleshooting.
## Security
CouchDB requires authentication on every request, and NPM's admin UI has its own login that you have to change the first time you use it. Keep this on your LAN unless you have deliberately set up remote access.
## Support
For problems with this add-on (not the upstream CouchDB or Nginx Proxy Manager software), create an issue on [github](https://github.com/alexbelgium/hassio-addons/issues) and tag @ToledoEM
- Obsidian Self-hosted LiveSync plugin → [github.com/vrtmrz/obsidian-livesync](https://github.com/vrtmrz/obsidian-livesync)
- CouchDB upstream → [couchdb.apache.org](https://couchdb.apache.org/)
- Nginx Proxy Manager upstream → [github.com/NginxProxyManager/nginx-proxy-manager](https://github.com/NginxProxyManager/nginx-proxy-manager)

View File

@@ -0,0 +1,37 @@
#include <tunables/global>
profile hassio-addons/obsidian_syncserver_npm flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/nameservice>
#include <abstractions/openssl>
# Baseline profile covering both CouchDB (Erlang VM) and Nginx Proxy
# Manager (s6-overlay, nginx, node). Both need broad file and network
# access plus the ability to drop privileges.
file,
network,
capability chown,
capability dac_override,
capability fowner,
capability kill,
capability net_bind_service,
capability setgid,
capability setuid,
signal (send) set=(kill,term,int,hup,cont),
# s6-overlay boot chain
/init ix,
/bin/** ix,
/usr/bin/** ix,
/command/** ix,
/package/** ix,
/run/{s6,s6-rc*,service}/** ix,
/etc/s6-overlay/** rwix,
deny /proc/kcore rwklx,
deny /proc/sysrq-trigger rwklx,
deny /sys/firmware/** rwklx,
}

View File

@@ -0,0 +1,4 @@
---
build_from:
aarch64: jc21/nginx-proxy-manager:2.15.1
amd64: jc21/nginx-proxy-manager:2.15.1

View File

@@ -0,0 +1,34 @@
name: "Obsidian Sync Server NPM"
slug: obsidian_syncserver_npm
image: ghcr.io/alexbelgium/obsidian_syncserver_npm-{arch}
description: "Self-hosted Obsidian LiveSync backend on CouchDB, bundled with Nginx Proxy Manager for TLS and certificate management."
version: "3.5.2.1"
url: "https://github.com/alexbelgium/hassio-addons/tree/master/obsidian_syncserver_npm"
arch:
- amd64
- aarch64
startup: services
init: false
ports:
5984/tcp: 5984
80/tcp: 80
81/tcp: 81
443/tcp: 443
ports_description:
5984/tcp: "CouchDB HTTP (direct access, desktop Obsidian)"
80/tcp: "HTTP (certificate validation and redirect)"
81/tcp: "Nginx Proxy Manager admin UI"
443/tcp: "HTTPS — use this for mobile Obsidian"
webui: "http://[HOST]:[PORT:81]"
map:
- addon_config:rw
options:
username: admin
password: ""
database: obsidian
log_level: info
schema:
username: str
password: password?
database: match(^[a-z][a-z0-9_$()+/-]*$)
log_level: list(debug|info|warn|error)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,20 @@
#!/command/with-contenv bash
# shellcheck shell=bash
# NPM's own "prepare" service aborts the entire boot if /etc/letsencrypt is
# not a mount. Home Assistant does not mount it, so point it at /data, which
# Supervisor persists as a Docker volume.
#
# This is an s6-rc oneshot that "prepare" depends on, so it is guaranteed to
# run first. NPM's image uses s6-overlay v3 and has no /etc/cont-init.d.
if [ ! -L /etc/letsencrypt ]; then
mkdir -p /data/letsencrypt
if [ -d /etc/letsencrypt ] && [ -n "$(ls -A /etc/letsencrypt 2> /dev/null)" ]; then
cp -a /etc/letsencrypt/. /data/letsencrypt/ 2> /dev/null || true
fi
rm -rf /etc/letsencrypt
ln -sf /data/letsencrypt /etc/letsencrypt
fi
mkdir -p /data/letsencrypt
echo "[obsidian-syncserver] /etc/letsencrypt -> /data/letsencrypt"

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/addon-init/script

View File

@@ -0,0 +1,3 @@
#!/command/with-contenv bash
# shellcheck shell=bash
exit 0

View File

@@ -0,0 +1,3 @@
#!/command/with-contenv bash
# shellcheck shell=bash
exec /run.sh

View File

@@ -0,0 +1 @@
longrun

282
obsidian_syncserver_npm/run.sh Executable file
View File

@@ -0,0 +1,282 @@
#!/command/with-contenv bash
# shellcheck shell=bash
set -Eeuo pipefail
# Obsidian LiveSync sync server (CouchDB) as a Home Assistant add-on.
#
# This flavour runs under the s6-overlay supervision tree that Nginx Proxy
# Manager's image provides, as one service alongside NPM's own. NPM handles
# TLS termination and certificate management; CouchDB only listens on
# loopback plus the LAN port.
#
# CouchDB alone is not usable as a LiveSync backend: the plugin needs a
# single-node cluster, CORS opened to Obsidian's app origins, authentication
# required, and raised request/document size limits. This script applies that
# configuration on every start. The settings mirror the upstream provisioning
# tool (vrtmrz/obsidian-livesync, utils/couchdb/provision.ts), which is the
# authoritative source for what LiveSync expects.
OPTIONS_JSON="/data/options.json"
ADDON_DIR="/config/obsidian-syncserver"
PASSWORD_FILE="${ADDON_DIR}/admin_password"
DATA_DIR="${ADDON_DIR}/data"
LOCAL_D="/opt/couchdb/etc/local.d"
COUCH_URL="http://127.0.0.1:5984"
# Retry budget matches provision.ts: CouchDB on a Raspberry Pi can take a
# while to open its listener on first boot.
READY_RETRIES=12
READY_DELAY=5
log() { echo "[obsidian-syncserver] $*"; }
warn() { echo "[obsidian-syncserver] WARN: $*" >&2; }
die() {
echo "[obsidian-syncserver] ERROR: $*" >&2
exit 1
}
read_opt() {
jq -er --arg k "$1" '.[$k]' "$OPTIONS_JSON" 2> /dev/null || true
}
# ---------------------------------------------------------------------------
# Step 1: Read add-on options
# ---------------------------------------------------------------------------
[[ -f "$OPTIONS_JSON" ]] || die "Missing options file at ${OPTIONS_JSON}"
USERNAME="$(read_opt username)"
USERNAME="${USERNAME:-admin}"
PASSWORD="$(read_opt password)"
DATABASE="$(read_opt database)"
DATABASE="${DATABASE:-obsidian}"
LOG_LEVEL="$(read_opt log_level)"
LOG_LEVEL="${LOG_LEVEL:-info}"
# CouchDB database names are restricted; a bad name only fails much later at
# the create step, with an opaque 400.
[[ "$DATABASE" =~ ^[a-z][a-z0-9_$()+/-]*$ ]] \
|| die "database '${DATABASE}' is invalid. Must start with a lowercase letter and contain only a-z 0-9 _ \$ ( ) + / -"
mkdir -p "$ADDON_DIR"
# ---------------------------------------------------------------------------
# Step 2: Resolve admin credentials
#
# A blank password auto-generates one and persists it, so the add-on never
# ships a guessable default. It is reused on later starts, otherwise every
# restart would invalidate the credentials already configured in Obsidian.
# ---------------------------------------------------------------------------
if [[ -z "$PASSWORD" ]]; then
if [[ -f "$PASSWORD_FILE" ]]; then
PASSWORD="$(cat "$PASSWORD_FILE")"
log "Using previously generated admin password from ${PASSWORD_FILE}"
else
PASSWORD="$(openssl rand -base64 24)"
(
umask 077
printf '%s\n' "$PASSWORD" > "$PASSWORD_FILE"
)
warn "No password set. Generated one and saved it to ${PASSWORD_FILE}"
warn "Admin username: ${USERNAME}"
warn "Admin password: ${PASSWORD}"
warn "Set a password in the add-on options to choose your own."
fi
fi
export COUCHDB_USER="$USERNAME"
export COUCHDB_PASSWORD="$PASSWORD"
# ---------------------------------------------------------------------------
# Step 3: Point CouchDB at persistent storage
#
# /data is wiped when the add-on is reinstalled, and is not included in a
# Home Assistant backup the way the add-on config directory is. The vault is
# the whole point of this add-on, so it lives under /config instead.
# ---------------------------------------------------------------------------
mkdir -p "$DATA_DIR" "${DATA_DIR}/.delayed" "$LOCAL_D"
COUCH_UID="$(id -u couchdb 2> /dev/null || echo 5984)"
COUCH_GID="$(id -g couchdb 2> /dev/null || echo 5984)"
chown -R "${COUCH_UID}:${COUCH_GID}" "$ADDON_DIR" 2> /dev/null \
|| warn "Could not chown ${ADDON_DIR}; CouchDB may fail to write to it"
cat > "${LOCAL_D}/10-addon-storage.ini" << EOF
; Managed by the Home Assistant add-on. Edits are overwritten on restart.
[couchdb]
database_dir = ${DATA_DIR}
view_index_dir = ${DATA_DIR}
[chttpd]
bind_address = 0.0.0.0
port = 5984
[log]
level = ${LOG_LEVEL}
EOF
# ---------------------------------------------------------------------------
# Step 3b: Seed a default nginx server block pointing at CouchDB
#
# NPM's own proxy hosts are created through its admin UI and stored in its
# database. This only replaces the "Congratulations" default page, so that a
# fresh install answers on 443 with CouchDB instead of NPM's placeholder.
# Once a real proxy host is defined in the UI, that takes precedence.
#
# The proxy settings below are what LiveSync needs: the Authorization header
# must survive the hop (CouchDB authenticates every request), and the
# connection must be upgradable (replication is long-lived).
# ---------------------------------------------------------------------------
DEFAULT_HOST_DIR="/data/nginx/default_host"
mkdir -p "$DEFAULT_HOST_DIR"
# nginx.conf includes this directory at http level, so this must be a whole
# server block rather than a bare location.
cat > "${DEFAULT_HOST_DIR}/obsidian_syncserver.conf" << 'EOF'
# Managed by the Home Assistant add-on. Edits are overwritten on restart.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
access_log /data/logs/obsidian_access.log standard;
error_log /data/logs/obsidian_error.log warn;
include conf.d/include/letsencrypt-acme-challenge.conf;
location / {
proxy_pass http://127.0.0.1:5984;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CouchDB authenticates every request, so the credentials must
# pass through untouched.
proxy_pass_request_headers on;
# LiveSync replication holds connections open.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 600s;
client_max_body_size 0;
}
}
EOF
log "Seeded default nginx host proxying to CouchDB on 127.0.0.1:5984"
# ---------------------------------------------------------------------------
# Step 4: Start CouchDB in the background
#
# It runs in the background so provisioning can talk to it, then this script
# blocks on it at the end. s6 supervises this script as the service, so it
# must not exit while CouchDB is alive.
# ---------------------------------------------------------------------------
log "Starting CouchDB (user=${USERNAME}, database=${DATABASE}, log_level=${LOG_LEVEL})"
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
COUCH_PID=$!
# Without this, a CouchDB that dies during provisioning leaves the script
# retrying against a socket that will never come up.
trap 'kill -TERM "$COUCH_PID" 2>/dev/null || true' EXIT INT TERM
# ---------------------------------------------------------------------------
# Step 5: Wait for CouchDB to accept requests
# ---------------------------------------------------------------------------
ready=false
for i in $(seq 1 "$READY_RETRIES"); do
if curl -fsS -u "${USERNAME}:${PASSWORD}" "${COUCH_URL}/_up" > /dev/null 2>&1; then
ready=true
break
fi
kill -0 "$COUCH_PID" 2> /dev/null || die "CouchDB exited during startup. See the log above."
log "Waiting for CouchDB to come up (${i}/${READY_RETRIES})"
sleep "$READY_DELAY"
done
[[ "$ready" == "true" ]] || die "CouchDB did not become ready after $((READY_RETRIES * READY_DELAY))s"
log "CouchDB is up, applying Obsidian LiveSync configuration"
# ---------------------------------------------------------------------------
# Step 6: Provision for LiveSync
#
# Every call below is idempotent, so this runs safely on each start and
# repairs configuration that was changed by hand in Fauxton.
# ---------------------------------------------------------------------------
# Promotes the single node out of the uninitialised state. A node that is
# already set up answers 400/409 with "already"/"finished", which is success
# here, not an error.
cluster_body="$(jq -nc \
--arg u "$USERNAME" --arg p "$PASSWORD" \
'{action:"enable_single_node",username:$u,password:$p,bind_address:"0.0.0.0",port:5984,singlenode:true}')"
cluster_response="$(curl -sS -u "${USERNAME}:${PASSWORD}" \
-X POST "${COUCH_URL}/_cluster_setup" \
-H "Content-Type: application/json" \
-d "$cluster_body" \
-w '\n%{http_code}' 2>&1 || true)"
cluster_code="$(printf '%s' "$cluster_response" | tail -n1)"
cluster_text="$(printf '%s' "$cluster_response" | sed '$d')"
case "$cluster_code" in
2*) log "Single-node cluster initialised" ;;
400 | 409)
if printf '%s' "$cluster_text" | rg -qi 'already|finished'; then
log "Single-node cluster already initialised"
else
die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}"
fi
;;
*) die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}" ;;
esac
# CORS origins are what let the Obsidian desktop app and the mobile app talk
# to CouchDB at all; without them the plugin fails with an opaque network
# error. Values are taken from provision.ts.
set_config() {
local label="$1" key="$2" value="$3" code
code="$(curl -sS -o /dev/null -w '%{http_code}' \
-u "${USERNAME}:${PASSWORD}" \
-X PUT "${COUCH_URL}/_node/_local/_config/${key}" \
-H "Content-Type: application/json" \
-d "$value" 2>&1 || true)"
case "$code" in
2*) log " set ${label}" ;;
*) die "Failed to ${label} (HTTP ${code}) at ${key}" ;;
esac
}
set_config "require authenticated HTTP users" "chttpd/require_valid_user" '"true"'
set_config "require authenticated HTTP users for authentication" "chttpd_auth/require_valid_user" '"true"'
set_config "the HTTP authentication challenge" "httpd/WWW-Authenticate" '"Basic realm=\"couchdb\""'
set_config "enable HTTP CORS" "httpd/enable_cors" '"true"'
set_config "enable clustered HTTP CORS" "chttpd/enable_cors" '"true"'
set_config "the maximum HTTP request size" "chttpd/max_http_request_size" '"4294967296"'
set_config "the maximum document size" "couchdb/max_document_size" '"50000000"'
set_config "enable CORS credentials" "cors/credentials" '"true"'
set_config "allowed CORS origins" "cors/origins" '"app://obsidian.md,capacitor://localhost,http://localhost"'
# 412 means the database is already there, which is the normal case on every
# start after the first.
db_code="$(curl -sS -o /dev/null -w '%{http_code}' \
-u "${USERNAME}:${PASSWORD}" \
-X PUT "${COUCH_URL}/$(printf '%s' "$DATABASE" | jq -sRr @uri)" 2>&1 || true)"
case "$db_code" in
2*) log "Created database '${DATABASE}'" ;;
412) log "Database '${DATABASE}' already exists" ;;
*) die "Failed to create database '${DATABASE}' (HTTP ${db_code})" ;;
esac
log "Ready. Point Obsidian Self-hosted LiveSync at this server."
log " database: ${DATABASE} username: ${USERNAME}"
# ---------------------------------------------------------------------------
# Step 7: Hand the container's lifetime back to CouchDB
# ---------------------------------------------------------------------------
trap - EXIT
wait "$COUCH_PID"

View File

@@ -0,0 +1,13 @@
configuration:
username:
name: Admin username
description: CouchDB administrator username. Use this in the Obsidian LiveSync plugin settings. Unrelated to the Nginx Proxy Manager login.
password:
name: Admin password
description: CouchDB administrator password. Leave blank to generate a strong one on first start and save it to /config/obsidian-syncserver/admin_password (also printed once in the log).
database:
name: Database name
description: CouchDB database holding the vault. Created automatically if missing. Must start with a lowercase letter.
log_level:
name: Log level
description: CouchDB log verbosity.

View File

@@ -0,0 +1,9 @@
{
"source": "dockerhub",
"upstream_repo": "library/couchdb",
"upstream_version": "3.5.2",
"last_update": "2026-08-12",
"paused": true,
"paused_reason": "Tracks two upstreams (couchdb and jc21/nginx-proxy-manager) which the updater cannot resolve together, and inherits the NPM base image build constraint from issue #4. Bump by hand.",
"slug": "obsidian_syncserver_npm"
}

View File

@@ -0,0 +1,11 @@
# Changelog
## 3.5.2
- Initial release wrapping couchdb:3.5.2 as an Obsidian Self-hosted LiveSync backend
- Applies the CouchDB configuration LiveSync requires on every start: single-node cluster, CORS for Obsidian app origins, mandatory authentication, 4 GB max request size, 50 MB max document size
- Creates the vault database automatically
- Generates and persists a strong admin password when none is set
- Stores data under /config/obsidian-syncserver/data so it survives reinstalls and is included in Home Assistant backups
- Plain HTTP on port 5984; use a reverse proxy for TLS if you need mobile sync

View File

@@ -0,0 +1,98 @@
# Obsidian Sync Server
CouchDB set up as a backend for the [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) plugin in Obsidian.
## What the add-on configures
A stock CouchDB will not work as a LiveSync backend. On every start this add-on applies the settings the plugin needs, matching what upstream's own provisioning tool does:
| Setting | Value | Why |
| :--- | :--- | :--- |
| `chttpd/require_valid_user` | `true` | No anonymous access |
| `chttpd_auth/require_valid_user` | `true` | No anonymous access to the auth endpoints |
| `httpd/WWW-Authenticate` | `Basic realm="couchdb"` | Prompts for credentials |
| `httpd/enable_cors`, `chttpd/enable_cors` | `true` | Obsidian behaves like a browser client |
| `cors/credentials` | `true` | Lets it send the auth header cross-origin |
| `cors/origins` | `app://obsidian.md,capacitor://localhost,http://localhost` | Desktop and mobile app origins |
| `chttpd/max_http_request_size` | `4294967296` | Large vault batches |
| `couchdb/max_document_size` | `50000000` | Large notes and attachments |
These get re-applied on each start, so editing them by hand in Fauxton will not stick.
## Storage
The vault database lives in `/config/obsidian-syncserver/data` rather than the add-on's `/data` directory, so it survives a reinstall and **gets picked up by Home Assistant backups**.
If you did not set a password, the generated one is in `/config/obsidian-syncserver/admin_password`.
## Reverse proxy setup
Mobile Obsidian refuses plain HTTP, so a phone or tablet needs TLS in front of this add-on. Any proxy will do, as long as it does three things:
Pass the `Authorization` header through untouched. CouchDB authenticates every single request, so a proxy that strips or rewrites that header turns everything into a 401.
Allow WebSocket upgrades. LiveSync uses continuous replication. Without upgrade support the connection looks like it works and then just sits there.
Avoid buffering responses indefinitely, or the long-poll changes feed lags behind.
### Nginx Proxy Manager
Add a Proxy Host:
- Domain Names: whatever hostname you plan to use, say `obsidian.example.com`
- Scheme: `http`
- Forward Hostname / IP: your Home Assistant machine
- Forward Port: `5984`
- Websockets Support: on
- On the SSL tab, request or select a certificate and turn on Force SSL
Then point LiveSync at `https://obsidian.example.com`.
### Plain nginx
```nginx
location / {
proxy_pass http://homeassistant.local:5984;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# CouchDB authenticates every request
proxy_pass_request_headers on;
# LiveSync uses continuous replication
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 600s;
}
```
## Troubleshooting
If the add-on stops right after starting, read the log. A malformed `database` name or a `/config` directory CouchDB cannot write to will both halt startup with a message saying which.
If LiveSync reports a network or CORS error, it is nearly always the proxy rather than CouchDB. Check the server directly first:
```bash
curl -u admin:YOURPASSWORD http://homeassistant.local:5984/obsidian
```
When that works but the plugin still fails, the proxy is either dropping the `Authorization` header or blocking the WebSocket upgrade.
If desktop syncs but mobile does not, the app does not trust your certificate. Self-signed ones generally will not cut it. The NPM version of this add-on exists partly to make that easier.
If sync connects and then stalls, WebSocket upgrade is not getting through the proxy.
To see the applied configuration:
```bash
curl -u admin:YOURPASSWORD http://homeassistant.local:5984/_node/_local/_config/cors
```
The Obsidian origins should be listed there.
## Backups
Home Assistant backs up `/config`, which covers the vault database. For a copy you can move elsewhere, use CouchDB replication or export from Fauxton at `http://<host>:5984/_utils`.

View File

@@ -0,0 +1,26 @@
ARG BUILD_FROM=couchdb:3.5.2
FROM ${BUILD_FROM}
# The official couchdb image is Debian-based and carries no s6-overlay, so
# this add-on runs run.sh directly rather than through the s6 service tree
# the other add-ons in this repository use.
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
jq \
openssl \
ripgrep \
&& rm -rf /var/lib/apt/lists/*
COPY run.sh /run.sh
RUN chmod +x /run.sh
ARG BUILD_VERSION
LABEL \
io.hass.version="${BUILD_VERSION}" \
io.hass.type="addon" \
io.hass.arch="aarch64|amd64"
ENTRYPOINT ["/run.sh"]

View File

@@ -0,0 +1,73 @@
# Home assistant add-on: Obsidian Sync Server
![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_solo%2Fconfig.yaml)
![Ingress](https://img.shields.io/badge/dynamic/yaml?label=Ingress&query=%24.ingress&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_solo%2Fconfig.yaml)
![Arch](https://img.shields.io/badge/dynamic/yaml?color=success&label=Arch&query=%24.arch&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_solo%2Fconfig.yaml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9c6cf10bdbba45ecb202d7f579b5be0e)](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
[![GitHub Super-Linter](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/weekly-supelinter.yaml?label=Lint%20code%20base)](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
[![Builder](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/onpush_builder.yaml?label=Builder)](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
Runs CouchDB as a sync backend for the [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) plugin in Obsidian.
This add-on is only the server side. Install the companion plugin in Obsidian: <https://community.obsidian.md/plugins/obsidian-livesync>
> [!IMPORTANT]
>
> - Before installing or upgrading this add-on or the obsidian livesync plugin, please back up your vault.
> - Not compatible with other synchronisation solution (including iCloud and Obsidian Sync).
> - For backups, use [Differential ZIP Backup](https://github.com/vrtmrz/diffzip).
Your vault syncs between your own devices through Home Assistant. No Obsidian Sync subscription, and the notes stay on your network.
This add-on speaks plain HTTP. Desktop Obsidian works fine with that. Mobile Obsidian does not, because it insists on a valid TLS certificate ([Easy to add on HA](https://www.home-assistant.io/blog/2017/09/27/effortless-encryption-with-lets-encrypt-and-duckdns/)). To sync a phone or tablet you need a reverse proxy in front of this add-on, or one of the other two versions:
| Add-on | TLS | Use when |
| :--- | :--- | :--- |
| Obsidian Sync Server (this one) | none | You already run a reverse proxy |
| [Obsidian Sync Server SSL](../obsidian_syncserver_ssl/README.md) | CouchDB serves HTTPS from your certificates in `/ssl` | You have certificates on the Home Assistant machine |
| [Obsidian Sync Server NPM](../obsidian_syncserver_npm/README.md) | Bundled Nginx Proxy Manager | You have no proxy and want certificate handling included |
## Installation
1. Add the repository `https://github.com/alexbelgium/hassio-addons` to Home Assistant, then install the add-on.
2. Set a password under Configuration. Leaving it blank generates one and prints it in the log on first start.
3. Start the add-on and look for `Ready.` in the log.
## Configuration
```yaml
username: admin
password: ""
database: obsidian
log_level: info
```
`username` and `password` are the CouchDB administrator credentials that the LiveSync plugin uses. A blank password gets generated on first start and saved to `/config/obsidian-syncserver/admin_password`.
`database` is the CouchDB database holding your vault. The add-on creates it if it does not exist.
`log_level` sets CouchDB log verbosity.
## Connecting Obsidian
Install Self-hosted LiveSync from Obsidian's community plugins. In its settings, pick the manual setup and fill in:
- URI: `http://<home-assistant-host>:5984`, or your proxy's HTTPS address
- Username and password: whatever you configured above
- Database name: `obsidian`, unless you changed it
Hit Test Database Connection to check it, then turn on end-to-end encryption with a passphrase. With that on, the server only ever holds ciphertext.
[DOCS.md](DOCS.md) covers reverse proxy setup and troubleshooting.
## Security
CouchDB here requires authentication on every request, so nothing is readable anonymously. Still, do not forward port 5984 to the internet. Keep it on your LAN, or put it behind a proxy that terminates TLS and does its own access control.
## Support
Create an issue on [github](https://github.com/alexbelgium/hassio-addons/issues) and tag @ToledoEM
- Obsidian Self-hosted LiveSync plugin → [github.com/vrtmrz/obsidian-livesync](https://github.com/vrtmrz/obsidian-livesync)
- CouchDB upstream → [couchdb.apache.org](https://couchdb.apache.org/)

View File

@@ -0,0 +1,24 @@
#include <tunables/global>
profile hassio-addons/obsidian_syncserver_solo flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/nameservice>
#include <abstractions/openssl>
# Baseline profile for CouchDB in HAOS. CouchDB runs on the Erlang VM,
# which needs broad file and network access plus the ability to drop
# privileges to the couchdb user.
file,
network,
capability chown,
capability dac_override,
capability fowner,
capability setgid,
capability setuid,
deny /proc/kcore rwklx,
deny /proc/sysrq-trigger rwklx,
deny /sys/firmware/** rwklx,
}

View File

@@ -0,0 +1,4 @@
---
build_from:
aarch64: couchdb:3.5.2
amd64: couchdb:3.5.2

View File

@@ -0,0 +1,27 @@
name: "Obsidian Sync Server"
slug: obsidian_syncserver_solo
image: ghcr.io/alexbelgium/obsidian_syncserver_solo-{arch}
description: "Self-hosted Obsidian LiveSync backend on CouchDB. Plain HTTP — put your own reverse proxy in front for TLS."
version: "3.5.2"
url: "https://github.com/alexbelgium/hassio-addons/tree/master/obsidian_syncserver_solo"
arch:
- amd64
- aarch64
startup: services
init: false
ports:
5984/tcp: 5984
ports_description:
5984/tcp: "CouchDB HTTP — add a reverse proxy for mobile Obsidian"
map:
- addon_config:rw
options:
username: admin
password: ""
database: obsidian
log_level: info
schema:
username: str
password: password?
database: match(^[a-z][a-z0-9_$()+/-]*$)
log_level: list(debug|info|warn|error)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

224
obsidian_syncserver_solo/run.sh Executable file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env bash
# shellcheck shell=bash
set -Eeuo pipefail
# Obsidian LiveSync sync server (CouchDB) as a Home Assistant add-on.
#
# CouchDB alone is not usable as a LiveSync backend: the plugin needs a
# single-node cluster, CORS opened to Obsidian's app origins, authentication
# required, and raised request/document size limits. This script applies that
# configuration on every start. The settings mirror the upstream provisioning
# tool (vrtmrz/obsidian-livesync, utils/couchdb/provision.ts), which is the
# authoritative source for what LiveSync expects.
OPTIONS_JSON="/data/options.json"
ADDON_DIR="/config/obsidian-syncserver"
PASSWORD_FILE="${ADDON_DIR}/admin_password"
DATA_DIR="${ADDON_DIR}/data"
LOCAL_D="/opt/couchdb/etc/local.d"
COUCH_URL="http://127.0.0.1:5984"
# Retry budget matches provision.ts: CouchDB on a Raspberry Pi can take a
# while to open its listener on first boot.
READY_RETRIES=12
READY_DELAY=5
log() { echo "[obsidian-syncserver] $*"; }
warn() { echo "[obsidian-syncserver] WARN: $*" >&2; }
die() {
echo "[obsidian-syncserver] ERROR: $*" >&2
exit 1
}
read_opt() {
jq -er --arg k "$1" '.[$k]' "$OPTIONS_JSON" 2> /dev/null || true
}
# ---------------------------------------------------------------------------
# Step 1: Read add-on options
# ---------------------------------------------------------------------------
[[ -f "$OPTIONS_JSON" ]] || die "Missing options file at ${OPTIONS_JSON}"
USERNAME="$(read_opt username)"
USERNAME="${USERNAME:-admin}"
PASSWORD="$(read_opt password)"
DATABASE="$(read_opt database)"
DATABASE="${DATABASE:-obsidian}"
LOG_LEVEL="$(read_opt log_level)"
LOG_LEVEL="${LOG_LEVEL:-info}"
# CouchDB database names are restricted; a bad name only fails much later at
# the create step, with an opaque 400.
[[ "$DATABASE" =~ ^[a-z][a-z0-9_$()+/-]*$ ]] \
|| die "database '${DATABASE}' is invalid. Must start with a lowercase letter and contain only a-z 0-9 _ \$ ( ) + / -"
mkdir -p "$ADDON_DIR"
# ---------------------------------------------------------------------------
# Step 2: Resolve admin credentials
#
# A blank password auto-generates one and persists it, so the add-on never
# ships a guessable default. It is reused on later starts, otherwise every
# restart would invalidate the credentials already configured in Obsidian.
# ---------------------------------------------------------------------------
if [[ -z "$PASSWORD" ]]; then
if [[ -f "$PASSWORD_FILE" ]]; then
PASSWORD="$(cat "$PASSWORD_FILE")"
log "Using previously generated admin password from ${PASSWORD_FILE}"
else
PASSWORD="$(openssl rand -base64 24)"
(
umask 077
printf '%s\n' "$PASSWORD" > "$PASSWORD_FILE"
)
warn "No password set. Generated one and saved it to ${PASSWORD_FILE}"
warn "Admin username: ${USERNAME}"
warn "Admin password: ${PASSWORD}"
warn "Set a password in the add-on options to choose your own."
fi
fi
export COUCHDB_USER="$USERNAME"
export COUCHDB_PASSWORD="$PASSWORD"
# ---------------------------------------------------------------------------
# Step 3: Point CouchDB at persistent storage
#
# /data is wiped when the add-on is reinstalled, and is not included in a
# Home Assistant backup the way the add-on config directory is. The vault is
# the whole point of this add-on, so it lives under /config instead.
# ---------------------------------------------------------------------------
mkdir -p "$DATA_DIR" "${DATA_DIR}/.delayed" "$LOCAL_D"
COUCH_UID="$(id -u couchdb 2> /dev/null || echo 5984)"
COUCH_GID="$(id -g couchdb 2> /dev/null || echo 5984)"
chown -R "${COUCH_UID}:${COUCH_GID}" "$ADDON_DIR" 2> /dev/null \
|| warn "Could not chown ${ADDON_DIR}; CouchDB may fail to write to it"
cat > "${LOCAL_D}/10-addon-storage.ini" << EOF
; Managed by the Home Assistant add-on. Edits are overwritten on restart.
[couchdb]
database_dir = ${DATA_DIR}
view_index_dir = ${DATA_DIR}
[chttpd]
bind_address = 0.0.0.0
port = 5984
[log]
level = ${LOG_LEVEL}
EOF
# Flavour hook: _ssl overrides this to write 20-addon-ssl.ini after validating
# the user's certificates. No-op in this flavour.
if declare -f configure_tls > /dev/null 2>&1; then
configure_tls
fi
# ---------------------------------------------------------------------------
# Step 4: Start CouchDB in the background
# ---------------------------------------------------------------------------
log "Starting CouchDB (user=${USERNAME}, database=${DATABASE}, log_level=${LOG_LEVEL})"
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
COUCH_PID=$!
# Without this, a CouchDB that dies during provisioning leaves the script
# retrying against a socket that will never come up.
trap 'kill -TERM "$COUCH_PID" 2>/dev/null || true' EXIT INT TERM
# ---------------------------------------------------------------------------
# Step 5: Wait for CouchDB to accept requests
# ---------------------------------------------------------------------------
ready=false
for i in $(seq 1 "$READY_RETRIES"); do
if curl -fsS -u "${USERNAME}:${PASSWORD}" "${COUCH_URL}/_up" > /dev/null 2>&1; then
ready=true
break
fi
kill -0 "$COUCH_PID" 2> /dev/null || die "CouchDB exited during startup. See the log above."
log "Waiting for CouchDB to come up (${i}/${READY_RETRIES})"
sleep "$READY_DELAY"
done
[[ "$ready" == "true" ]] || die "CouchDB did not become ready after $((READY_RETRIES * READY_DELAY))s"
log "CouchDB is up, applying Obsidian LiveSync configuration"
# ---------------------------------------------------------------------------
# Step 6: Provision for LiveSync
#
# Every call below is idempotent, so this runs safely on each start and
# repairs configuration that was changed by hand in Fauxton.
# ---------------------------------------------------------------------------
# Promotes the single node out of the uninitialised state. A node that is
# already set up answers 400/409 with "already"/"finished", which is success
# here, not an error.
cluster_body="$(jq -nc \
--arg u "$USERNAME" --arg p "$PASSWORD" \
'{action:"enable_single_node",username:$u,password:$p,bind_address:"0.0.0.0",port:5984,singlenode:true}')"
cluster_response="$(curl -sS -u "${USERNAME}:${PASSWORD}" \
-X POST "${COUCH_URL}/_cluster_setup" \
-H "Content-Type: application/json" \
-d "$cluster_body" \
-w '\n%{http_code}' 2>&1 || true)"
cluster_code="$(printf '%s' "$cluster_response" | tail -n1)"
cluster_text="$(printf '%s' "$cluster_response" | sed '$d')"
case "$cluster_code" in
2*) log "Single-node cluster initialised" ;;
400 | 409)
if printf '%s' "$cluster_text" | rg -qi 'already|finished'; then
log "Single-node cluster already initialised"
else
die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}"
fi
;;
*) die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}" ;;
esac
# CORS origins are what let the Obsidian desktop app and the mobile app talk
# to CouchDB at all; without them the plugin fails with an opaque network
# error. Values are taken from provision.ts.
set_config() {
local label="$1" key="$2" value="$3" code
code="$(curl -sS -o /dev/null -w '%{http_code}' \
-u "${USERNAME}:${PASSWORD}" \
-X PUT "${COUCH_URL}/_node/_local/_config/${key}" \
-H "Content-Type: application/json" \
-d "$value" 2>&1 || true)"
case "$code" in
2*) log " set ${label}" ;;
*) die "Failed to ${label} (HTTP ${code}) at ${key}" ;;
esac
}
set_config "require authenticated HTTP users" "chttpd/require_valid_user" '"true"'
set_config "require authenticated HTTP users for authentication" "chttpd_auth/require_valid_user" '"true"'
set_config "the HTTP authentication challenge" "httpd/WWW-Authenticate" '"Basic realm=\"couchdb\""'
set_config "enable HTTP CORS" "httpd/enable_cors" '"true"'
set_config "enable clustered HTTP CORS" "chttpd/enable_cors" '"true"'
set_config "the maximum HTTP request size" "chttpd/max_http_request_size" '"4294967296"'
set_config "the maximum document size" "couchdb/max_document_size" '"50000000"'
set_config "enable CORS credentials" "cors/credentials" '"true"'
set_config "allowed CORS origins" "cors/origins" '"app://obsidian.md,capacitor://localhost,http://localhost"'
# 412 means the database is already there, which is the normal case on every
# start after the first.
db_code="$(curl -sS -o /dev/null -w '%{http_code}' \
-u "${USERNAME}:${PASSWORD}" \
-X PUT "${COUCH_URL}/$(printf '%s' "$DATABASE" | jq -sRr @uri)" 2>&1 || true)"
case "$db_code" in
2*) log "Created database '${DATABASE}'" ;;
412) log "Database '${DATABASE}' already exists" ;;
*) die "Failed to create database '${DATABASE}' (HTTP ${db_code})" ;;
esac
log "Ready. Point Obsidian Self-hosted LiveSync at this server."
log " database: ${DATABASE} username: ${USERNAME}"
# ---------------------------------------------------------------------------
# Step 7: Hand the container's lifetime back to CouchDB
# ---------------------------------------------------------------------------
trap - EXIT
wait "$COUCH_PID"

View File

@@ -0,0 +1,13 @@
configuration:
username:
name: Admin username
description: CouchDB administrator username. Use this in the Obsidian LiveSync plugin settings.
password:
name: Admin password
description: CouchDB administrator password. Leave blank to generate a strong one on first start and save it to /config/obsidian-syncserver/admin_password (also printed once in the log).
database:
name: Database name
description: CouchDB database holding the vault. Created automatically if missing. Must start with a lowercase letter.
log_level:
name: Log level
description: CouchDB log verbosity.

View File

@@ -0,0 +1,7 @@
{
"source": "dockerhub",
"upstream_repo": "library/couchdb",
"upstream_version": "3.5.2",
"last_update": "2026-08-12",
"slug": "obsidian_syncserver_solo"
}

View File

@@ -0,0 +1,13 @@
# Changelog
## 3.5.2
- Initial release wrapping couchdb:3.5.2 as an Obsidian Self-hosted LiveSync backend with native HTTPS
- Serves HTTPS on port 6984 using certificates supplied in /ssl, so mobile Obsidian can sync
- Validates the certificate before starting: presence, PEM parsing, expiry, and certificate/key match, each reported with its specific cause
- Logs the certificate's covered hostnames and expiry date
- Applies the CouchDB configuration LiveSync requires on every start: single-node cluster, CORS for Obsidian app origins, mandatory authentication, 4 GB max request size, 50 MB max document size
- Creates the vault database automatically
- Generates and persists a strong admin password when none is set
- Stores data under /config/obsidian-syncserver/data so it survives reinstalls and is included in Home Assistant backups

View File

@@ -0,0 +1,93 @@
# Obsidian Sync Server SSL
CouchDB set up as a backend for the [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) plugin in Obsidian, serving HTTPS directly from certificates in `/ssl`.
## Ports
| Port | Protocol | Use |
| :--- | :--- | :--- |
| 5984 | HTTP | Desktop Obsidian, Fauxton, local tools |
| 6984 | HTTPS | Mobile Obsidian, anything needing TLS |
Both are served at once. HTTPS only appears when `ssl` is on and the certificate passes its checks.
## Certificates
Certificates come from `/ssl`, mapped read-only. The Let's Encrypt and DuckDNS add-ons are the usual things writing them there.
This add-on never renews anything. It only reads. When the certificate expires the add-on refuses to start until whatever issued it renews the file. That is deliberate: quietly serving an expired certificate produces a sync failure on the phone with no explanation, which is far worse to track down than a stopped add-on with a clear message in the log.
### What gets checked before startup
| Check | The failure message names |
| :--- | :--- |
| File present and readable | The exact path it tried |
| Valid PEM certificate | The file that would not parse |
| Valid PEM private key | The file that would not parse |
| Not expired | The expiry date |
| Certificate matches key | Both filenames |
A good start logs the covered hostnames and the expiry date:
```
Certificate covers: obsidian.example.com
Obsidian must reach this server by one of those names, or it will reject the certificate.
TLS enabled on port 6984 (certificate valid until Nov 3 12:00:00 2026 GMT)
```
The hostname list is there to help you spot a mismatch, not as a hard check. Reaching the server by some other name is legitimate, so the add-on still starts.
## What the add-on configures
A stock CouchDB will not work as a LiveSync backend. On every start this add-on applies the settings the plugin needs, matching what upstream's own provisioning tool does:
| Setting | Value | Why |
| :--- | :--- | :--- |
| `chttpd/require_valid_user` | `true` | No anonymous access |
| `chttpd_auth/require_valid_user` | `true` | No anonymous access to the auth endpoints |
| `httpd/WWW-Authenticate` | `Basic realm="couchdb"` | Prompts for credentials |
| `httpd/enable_cors`, `chttpd/enable_cors` | `true` | Obsidian behaves like a browser client |
| `cors/credentials` | `true` | Lets it send the auth header cross-origin |
| `cors/origins` | `app://obsidian.md,capacitor://localhost,http://localhost` | Desktop and mobile app origins |
| `chttpd/max_http_request_size` | `4294967296` | Large vault batches |
| `couchdb/max_document_size` | `50000000` | Large notes and attachments |
These get re-applied on each start, so editing them by hand in Fauxton will not stick.
## Storage
The vault database lives in `/config/obsidian-syncserver/data` rather than the add-on's `/data` directory, so it survives a reinstall and **gets picked up by Home Assistant backups**.
If you did not set a password, the generated one is in `/config/obsidian-syncserver/admin_password`.
## Troubleshooting
If the add-on will not start and the log mentions the certificate, the message names the specific problem: missing file, unparseable PEM, expired, or a certificate and key that do not belong together. Fix the file in `/ssl`, or set `ssl` to `false` to run HTTP only while you sort it out.
For an expired certificate, renew it with whatever add-on issues it, then restart this one. Worth checking that the renewal is actually scheduled. A certificate that lapsed months ago usually means nothing is renewing it at all.
If desktop syncs but mobile does not, suspect the certificate. Check that the phone reaches the server by a hostname the certificate covers rather than by IP, and that the issuer is one the phone trusts. Self-signed certificates normally get rejected.
To confirm what is actually being served:
```bash
openssl s_client -connect yourhost:6984 </dev/null | openssl x509 -noout -subject -dates
```
To see the applied configuration:
```bash
curl -u admin:YOURPASSWORD https://yourhost:6984/_node/_local/_config/cors
```
If LiveSync reports a CORS or network error over HTTPS, check the plain HTTP port first:
```bash
curl -u admin:YOURPASSWORD http://homeassistant.local:5984/obsidian
```
HTTP working while HTTPS does not points at the certificate rather than CouchDB.
## Backups
Home Assistant backs up `/config`, which covers the vault database. For a copy you can move elsewhere, use CouchDB replication or export from Fauxton at `https://<host>:6984/_utils`.

View File

@@ -0,0 +1,26 @@
ARG BUILD_FROM=couchdb:3.5.2
FROM ${BUILD_FROM}
# The official couchdb image is Debian-based and carries no s6-overlay, so
# this add-on runs run.sh directly rather than through the s6 service tree
# the other add-ons in this repository use.
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
jq \
openssl \
ripgrep \
&& rm -rf /var/lib/apt/lists/*
COPY run.sh /run.sh
RUN chmod +x /run.sh
ARG BUILD_VERSION
LABEL \
io.hass.version="${BUILD_VERSION}" \
io.hass.type="addon" \
io.hass.arch="aarch64|amd64"
ENTRYPOINT ["/run.sh"]

View File

@@ -0,0 +1,95 @@
# Home assistant add-on: Obsidian Sync Server SSL
![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_ssl%2Fconfig.yaml)
![Ingress](https://img.shields.io/badge/dynamic/yaml?label=Ingress&query=%24.ingress&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_ssl%2Fconfig.yaml)
![Arch](https://img.shields.io/badge/dynamic/yaml?color=success&label=Arch&query=%24.arch&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_ssl%2Fconfig.yaml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9c6cf10bdbba45ecb202d7f579b5be0e)](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
[![GitHub Super-Linter](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/weekly-supelinter.yaml?label=Lint%20code%20base)](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
[![Builder](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/onpush_builder.yaml?label=Builder)](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
Runs CouchDB as a sync backend for the [Self-hosted LiveSync](https://github.com/vrtmrz/obsidian-livesync) plugin in Obsidian, serving HTTPS from certificates you already have.
This add-on is only the server side. Install the companion plugin in Obsidian: <https://community.obsidian.md/plugins/obsidian-livesync>
> [!IMPORTANT]
>
> - Before installing or upgrading this add-on or the obsidian livesync plugin, please back up your vault.
> - Not compatible with other synchronisation solution (including iCloud and Obsidian Sync).
> - For backups, use [Differential ZIP Backup](https://github.com/vrtmrz/diffzip).
Your vault syncs between your own devices through Home Assistant. No Obsidian Sync subscription, and the notes stay on your network.
This version serves HTTPS on port 6984 using certificates from `/ssl`, so mobile Obsidian can sync without a separate reverse proxy ([Easy to add on HA](https://www.home-assistant.io/blog/2017/09/27/effortless-encryption-with-lets-encrypt-and-duckdns/)).
| Add-on | TLS | Use when |
| :--- | :--- | :--- |
| [Obsidian Sync Server](../obsidian_syncserver_solo/README.md) | none | You already run a reverse proxy |
| Obsidian Sync Server SSL (this one) | CouchDB serves HTTPS from your certificates in `/ssl` | You have certificates on the Home Assistant machine |
| [Obsidian Sync Server NPM](../obsidian_syncserver_npm/README.md) | Bundled Nginx Proxy Manager | You have no proxy and want certificate handling included |
## What you need first
A certificate and private key in `/ssl` on the Home Assistant machine. The Let's Encrypt and DuckDNS add-ons both put them there. This add-on only reads them. It never requests or renews anything.
A self-signed certificate usually will not satisfy mobile Obsidian, which wants one it already trusts.
## Installation
1. Add the repository `https://github.com/alexbelgium/hassio-addons` to Home Assistant, then install the add-on.
2. Set a password under Configuration. Leaving it blank generates one and prints it in the log on first start.
3. Check that `certfile` and `keyfile` match the filenames sitting in `/ssl`.
4. Start the add-on. The log should show `TLS enabled on port 6984` and then `Ready.`
## Configuration
```yaml
username: admin
password: ""
database: obsidian
ssl: true
certfile: fullchain.pem
keyfile: privkey.pem
log_level: info
```
`username` and `password` are the CouchDB administrator credentials that the LiveSync plugin uses. A blank password gets generated on first start and saved to `/config/obsidian-syncserver/admin_password`.
`database` is the CouchDB database holding your vault. The add-on creates it if it does not exist.
`ssl` turns HTTPS on port 6984 on or off. With it off you get HTTP only, and mobile sync will not work.
`certfile` and `keyfile` are filenames inside `/ssl`.
`log_level` sets CouchDB log verbosity.
## Certificate checks
A broken certificate shows up on the client as an unexplained connection failure, which is miserable to debug. So the add-on checks the certificate before it starts and refuses to run if the file is missing, unreadable, not valid PEM, expired, or does not match the private key. Whichever it is, the log says so.
On a good start it prints the hostnames the certificate covers and the expiry date.
Obsidian has to reach the server by a name the certificate covers. Connecting by IP address when the certificate lists only DNS names will fail.
## Connecting Obsidian
Install Self-hosted LiveSync from Obsidian's community plugins. In its settings, pick the manual setup and fill in:
- URI: `https://<hostname-on-your-certificate>:6984`
- Username and password: whatever you configured above
- Database name: `obsidian`, unless you changed it
Hit Test Database Connection to check it, then turn on end-to-end encryption with a passphrase. With that on, the server only ever holds ciphertext.
[DOCS.md](DOCS.md) covers troubleshooting.
## Security
CouchDB here requires authentication on every request. Keep this on your LAN unless you have deliberately set up remote access.
## Support
Create an issue on [github](https://github.com/alexbelgium/hassio-addons/issues) and tag @ToledoEM
- Obsidian Self-hosted LiveSync plugin → [github.com/vrtmrz/obsidian-livesync](https://github.com/vrtmrz/obsidian-livesync)
- CouchDB upstream → [couchdb.apache.org](https://couchdb.apache.org/)

View File

@@ -0,0 +1,27 @@
#include <tunables/global>
profile hassio-addons/obsidian_syncserver_ssl flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/nameservice>
#include <abstractions/openssl>
# Baseline profile for CouchDB in HAOS. CouchDB runs on the Erlang VM,
# which needs broad file and network access plus the ability to drop
# privileges to the couchdb user.
file,
network,
capability chown,
capability dac_override,
capability fowner,
capability setgid,
capability setuid,
# Certificates are supplied by the user through the read-only /ssl map.
/ssl/** r,
deny /proc/kcore rwklx,
deny /proc/sysrq-trigger rwklx,
deny /sys/firmware/** rwklx,
}

View File

@@ -0,0 +1,4 @@
---
build_from:
aarch64: couchdb:3.5.2
amd64: couchdb:3.5.2

View File

@@ -0,0 +1,36 @@
name: "Obsidian Sync Server SSL"
slug: obsidian_syncserver_ssl
image: ghcr.io/alexbelgium/obsidian_syncserver_ssl-{arch}
description: "Self-hosted Obsidian LiveSync backend on CouchDB, serving HTTPS with your own certificates from /ssl. Supports mobile Obsidian."
version: "3.5.2"
url: "https://github.com/alexbelgium/hassio-addons/tree/master/obsidian_syncserver_ssl"
arch:
- amd64
- aarch64
startup: services
init: false
ports:
5984/tcp: 5984
6984/tcp: 6984
ports_description:
5984/tcp: "CouchDB HTTP"
6984/tcp: "CouchDB HTTPS — use this for mobile Obsidian"
map:
- addon_config:rw
- ssl:ro
options:
username: admin
password: ""
database: obsidian
ssl: true
certfile: fullchain.pem
keyfile: privkey.pem
log_level: info
schema:
username: str
password: password?
database: match(^[a-z][a-z0-9_$()+/-]*$)
ssl: bool
certfile: str
keyfile: str
log_level: list(debug|info|warn|error)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

287
obsidian_syncserver_ssl/run.sh Executable file
View File

@@ -0,0 +1,287 @@
#!/usr/bin/env bash
# shellcheck shell=bash
set -Eeuo pipefail
# Obsidian LiveSync sync server (CouchDB) as a Home Assistant add-on.
#
# CouchDB alone is not usable as a LiveSync backend: the plugin needs a
# single-node cluster, CORS opened to Obsidian's app origins, authentication
# required, and raised request/document size limits. This script applies that
# configuration on every start. The settings mirror the upstream provisioning
# tool (vrtmrz/obsidian-livesync, utils/couchdb/provision.ts), which is the
# authoritative source for what LiveSync expects.
OPTIONS_JSON="/data/options.json"
ADDON_DIR="/config/obsidian-syncserver"
PASSWORD_FILE="${ADDON_DIR}/admin_password"
DATA_DIR="${ADDON_DIR}/data"
LOCAL_D="/opt/couchdb/etc/local.d"
COUCH_URL="http://127.0.0.1:5984"
# Retry budget matches provision.ts: CouchDB on a Raspberry Pi can take a
# while to open its listener on first boot.
READY_RETRIES=12
READY_DELAY=5
log() { echo "[obsidian-syncserver] $*"; }
warn() { echo "[obsidian-syncserver] WARN: $*" >&2; }
die() {
echo "[obsidian-syncserver] ERROR: $*" >&2
exit 1
}
read_opt() {
jq -er --arg k "$1" '.[$k]' "$OPTIONS_JSON" 2> /dev/null || true
}
# ---------------------------------------------------------------------------
# Step 1: Read add-on options
# ---------------------------------------------------------------------------
[[ -f "$OPTIONS_JSON" ]] || die "Missing options file at ${OPTIONS_JSON}"
USERNAME="$(read_opt username)"
USERNAME="${USERNAME:-admin}"
PASSWORD="$(read_opt password)"
DATABASE="$(read_opt database)"
DATABASE="${DATABASE:-obsidian}"
LOG_LEVEL="$(read_opt log_level)"
LOG_LEVEL="${LOG_LEVEL:-info}"
SSL_ENABLED="$(read_opt ssl)"
SSL_ENABLED="${SSL_ENABLED:-true}"
CERTFILE="$(read_opt certfile)"
CERTFILE="${CERTFILE:-fullchain.pem}"
KEYFILE="$(read_opt keyfile)"
KEYFILE="${KEYFILE:-privkey.pem}"
# CouchDB database names are restricted; a bad name only fails much later at
# the create step, with an opaque 400.
[[ "$DATABASE" =~ ^[a-z][a-z0-9_$()+/-]*$ ]] \
|| die "database '${DATABASE}' is invalid. Must start with a lowercase letter and contain only a-z 0-9 _ \$ ( ) + / -"
mkdir -p "$ADDON_DIR"
# ---------------------------------------------------------------------------
# Step 2: Resolve admin credentials
#
# A blank password auto-generates one and persists it, so the add-on never
# ships a guessable default. It is reused on later starts, otherwise every
# restart would invalidate the credentials already configured in Obsidian.
# ---------------------------------------------------------------------------
if [[ -z "$PASSWORD" ]]; then
if [[ -f "$PASSWORD_FILE" ]]; then
PASSWORD="$(cat "$PASSWORD_FILE")"
log "Using previously generated admin password from ${PASSWORD_FILE}"
else
PASSWORD="$(openssl rand -base64 24)"
(
umask 077
printf '%s\n' "$PASSWORD" > "$PASSWORD_FILE"
)
warn "No password set. Generated one and saved it to ${PASSWORD_FILE}"
warn "Admin username: ${USERNAME}"
warn "Admin password: ${PASSWORD}"
warn "Set a password in the add-on options to choose your own."
fi
fi
export COUCHDB_USER="$USERNAME"
export COUCHDB_PASSWORD="$PASSWORD"
# ---------------------------------------------------------------------------
# Step 3: Point CouchDB at persistent storage
#
# /data is wiped when the add-on is reinstalled, and is not included in a
# Home Assistant backup the way the add-on config directory is. The vault is
# the whole point of this add-on, so it lives under /config instead.
# ---------------------------------------------------------------------------
mkdir -p "$DATA_DIR" "${DATA_DIR}/.delayed" "$LOCAL_D"
COUCH_UID="$(id -u couchdb 2> /dev/null || echo 5984)"
COUCH_GID="$(id -g couchdb 2> /dev/null || echo 5984)"
chown -R "${COUCH_UID}:${COUCH_GID}" "$ADDON_DIR" 2> /dev/null \
|| warn "Could not chown ${ADDON_DIR}; CouchDB may fail to write to it"
cat > "${LOCAL_D}/10-addon-storage.ini" << EOF
; Managed by the Home Assistant add-on. Edits are overwritten on restart.
[couchdb]
database_dir = ${DATA_DIR}
view_index_dir = ${DATA_DIR}
[chttpd]
bind_address = 0.0.0.0
port = 5984
[log]
level = ${LOG_LEVEL}
EOF
# ---------------------------------------------------------------------------
# Step 3b: Configure native TLS
#
# A certificate problem here surfaces on the client as an opaque "cannot
# connect" — mobile Obsidian gives no detail about why it rejected the
# server. So every failure mode is checked up front and reported with the
# specific cause, rather than starting a listener clients will refuse.
# ---------------------------------------------------------------------------
SSL_INI="${LOCAL_D}/20-addon-ssl.ini"
rm -f "$SSL_INI"
if [[ "$SSL_ENABLED" == "true" ]]; then
CERT_PATH="/ssl/${CERTFILE#/ssl/}"
KEY_PATH="/ssl/${KEYFILE#/ssl/}"
[[ -f "$CERT_PATH" ]] || die "certfile not found at ${CERT_PATH}. Check the certfile option, or set ssl to false."
[[ -r "$CERT_PATH" ]] || die "certfile at ${CERT_PATH} is not readable."
[[ -f "$KEY_PATH" ]] || die "keyfile not found at ${KEY_PATH}. Check the keyfile option, or set ssl to false."
[[ -r "$KEY_PATH" ]] || die "keyfile at ${KEY_PATH} is not readable."
openssl x509 -in "$CERT_PATH" -noout > /dev/null 2>&1 \
|| die "certfile at ${CERT_PATH} is not a valid PEM certificate."
openssl pkey -in "$KEY_PATH" -noout > /dev/null 2>&1 \
|| die "keyfile at ${KEY_PATH} is not a valid PEM private key."
# An expired certificate is the most common cause of "it worked last
# month and now my phone will not sync".
if ! openssl x509 -in "$CERT_PATH" -checkend 0 -noout > /dev/null 2>&1; then
not_after="$(openssl x509 -in "$CERT_PATH" -noout -enddate 2> /dev/null | cut -d= -f2-)"
die "certfile at ${CERT_PATH} expired on ${not_after}. Renew it, or set ssl to false to serve HTTP only."
fi
# A mismatched pair starts fine and then fails every handshake.
cert_pub="$(openssl x509 -in "$CERT_PATH" -noout -pubkey 2> /dev/null | openssl md5 2> /dev/null)"
key_pub="$(openssl pkey -in "$KEY_PATH" -pubout 2> /dev/null | openssl md5 2> /dev/null)"
[[ -n "$cert_pub" && "$cert_pub" == "$key_pub" ]] \
|| die "certfile and keyfile do not match — ${CERTFILE} was not issued for ${KEYFILE}."
# Warn only: hostname detection is best effort, and a mismatch is
# legitimate when reaching the server by an alternate name.
san="$(openssl x509 -in "$CERT_PATH" -noout -ext subjectAltName 2> /dev/null | rg -o 'DNS:[^,]+' | sed 's/DNS://' | tr '\n' ' ' || true)"
if [[ -n "$san" ]]; then
log "Certificate covers: ${san}"
log "Obsidian must reach this server by one of those names, or it will reject the certificate."
fi
expires="$(openssl x509 -in "$CERT_PATH" -noout -enddate 2> /dev/null | cut -d= -f2-)"
log "TLS enabled on port 6984 (certificate valid until ${expires})"
cat > "$SSL_INI" << EOF
; Managed by the Home Assistant add-on. Edits are overwritten on restart.
[ssl]
enable = true
cert_file = ${CERT_PATH}
key_file = ${KEY_PATH}
port = 6984
bind_address = 0.0.0.0
EOF
else
warn "TLS is disabled. Mobile Obsidian requires HTTPS and will not be able to sync."
warn "Set ssl to true with a valid certificate in /ssl to enable it."
fi
# ---------------------------------------------------------------------------
# Step 4: Start CouchDB in the background
# ---------------------------------------------------------------------------
log "Starting CouchDB (user=${USERNAME}, database=${DATABASE}, log_level=${LOG_LEVEL})"
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
COUCH_PID=$!
# Without this, a CouchDB that dies during provisioning leaves the script
# retrying against a socket that will never come up.
trap 'kill -TERM "$COUCH_PID" 2>/dev/null || true' EXIT INT TERM
# ---------------------------------------------------------------------------
# Step 5: Wait for CouchDB to accept requests
# ---------------------------------------------------------------------------
ready=false
for i in $(seq 1 "$READY_RETRIES"); do
if curl -fsS -u "${USERNAME}:${PASSWORD}" "${COUCH_URL}/_up" > /dev/null 2>&1; then
ready=true
break
fi
kill -0 "$COUCH_PID" 2> /dev/null || die "CouchDB exited during startup. See the log above."
log "Waiting for CouchDB to come up (${i}/${READY_RETRIES})"
sleep "$READY_DELAY"
done
[[ "$ready" == "true" ]] || die "CouchDB did not become ready after $((READY_RETRIES * READY_DELAY))s"
log "CouchDB is up, applying Obsidian LiveSync configuration"
# ---------------------------------------------------------------------------
# Step 6: Provision for LiveSync
#
# Every call below is idempotent, so this runs safely on each start and
# repairs configuration that was changed by hand in Fauxton.
# ---------------------------------------------------------------------------
# Promotes the single node out of the uninitialised state. A node that is
# already set up answers 400/409 with "already"/"finished", which is success
# here, not an error.
cluster_body="$(jq -nc \
--arg u "$USERNAME" --arg p "$PASSWORD" \
'{action:"enable_single_node",username:$u,password:$p,bind_address:"0.0.0.0",port:5984,singlenode:true}')"
cluster_response="$(curl -sS -u "${USERNAME}:${PASSWORD}" \
-X POST "${COUCH_URL}/_cluster_setup" \
-H "Content-Type: application/json" \
-d "$cluster_body" \
-w '\n%{http_code}' 2>&1 || true)"
cluster_code="$(printf '%s' "$cluster_response" | tail -n1)"
cluster_text="$(printf '%s' "$cluster_response" | sed '$d')"
case "$cluster_code" in
2*) log "Single-node cluster initialised" ;;
400 | 409)
if printf '%s' "$cluster_text" | rg -qi 'already|finished'; then
log "Single-node cluster already initialised"
else
die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}"
fi
;;
*) die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}" ;;
esac
# CORS origins are what let the Obsidian desktop app and the mobile app talk
# to CouchDB at all; without them the plugin fails with an opaque network
# error. Values are taken from provision.ts.
set_config() {
local label="$1" key="$2" value="$3" code
code="$(curl -sS -o /dev/null -w '%{http_code}' \
-u "${USERNAME}:${PASSWORD}" \
-X PUT "${COUCH_URL}/_node/_local/_config/${key}" \
-H "Content-Type: application/json" \
-d "$value" 2>&1 || true)"
case "$code" in
2*) log " set ${label}" ;;
*) die "Failed to ${label} (HTTP ${code}) at ${key}" ;;
esac
}
set_config "require authenticated HTTP users" "chttpd/require_valid_user" '"true"'
set_config "require authenticated HTTP users for authentication" "chttpd_auth/require_valid_user" '"true"'
set_config "the HTTP authentication challenge" "httpd/WWW-Authenticate" '"Basic realm=\"couchdb\""'
set_config "enable HTTP CORS" "httpd/enable_cors" '"true"'
set_config "enable clustered HTTP CORS" "chttpd/enable_cors" '"true"'
set_config "the maximum HTTP request size" "chttpd/max_http_request_size" '"4294967296"'
set_config "the maximum document size" "couchdb/max_document_size" '"50000000"'
set_config "enable CORS credentials" "cors/credentials" '"true"'
set_config "allowed CORS origins" "cors/origins" '"app://obsidian.md,capacitor://localhost,http://localhost"'
# 412 means the database is already there, which is the normal case on every
# start after the first.
db_code="$(curl -sS -o /dev/null -w '%{http_code}' \
-u "${USERNAME}:${PASSWORD}" \
-X PUT "${COUCH_URL}/$(printf '%s' "$DATABASE" | jq -sRr @uri)" 2>&1 || true)"
case "$db_code" in
2*) log "Created database '${DATABASE}'" ;;
412) log "Database '${DATABASE}' already exists" ;;
*) die "Failed to create database '${DATABASE}' (HTTP ${db_code})" ;;
esac
log "Ready. Point Obsidian Self-hosted LiveSync at this server."
log " database: ${DATABASE} username: ${USERNAME}"
# ---------------------------------------------------------------------------
# Step 7: Hand the container's lifetime back to CouchDB
# ---------------------------------------------------------------------------
trap - EXIT
wait "$COUCH_PID"

View File

@@ -0,0 +1,22 @@
configuration:
username:
name: Admin username
description: CouchDB administrator username. Use this in the Obsidian LiveSync plugin settings.
password:
name: Admin password
description: CouchDB administrator password. Leave blank to generate a strong one on first start and save it to /config/obsidian-syncserver/admin_password (also printed once in the log).
database:
name: Database name
description: CouchDB database holding the vault. Created automatically if missing. Must start with a lowercase letter.
ssl:
name: Enable HTTPS
description: Serve HTTPS on port 6984 using the certificate below. Required for mobile Obsidian. The add-on refuses to start if the certificate is missing, expired, or does not match the key.
certfile:
name: Certificate file
description: Certificate filename inside /ssl, typically fullchain.pem. Written there by the Let's Encrypt or DuckDNS add-on; this add-on only reads it and never renews it.
keyfile:
name: Private key file
description: Private key filename inside /ssl, typically privkey.pem.
log_level:
name: Log level
description: CouchDB log verbosity.

View File

@@ -0,0 +1,7 @@
{
"source": "dockerhub",
"upstream_repo": "library/couchdb",
"upstream_version": "3.5.2",
"last_update": "2026-08-12",
"slug": "obsidian_syncserver_ssl"
}