Compare commits

...

24 Commits

Author SHA1 Message Date
github-actions
8e7c087a49 Github bot : issues linked to readme 2026-08-13 06:10:42 +00:00
Alexandre
19d36c2d33 fix(qbittorrent): allow ::/0 so WireGuard stops dropping IPv6 traffic (#2970)
* fix(qbittorrent): allow ::/0 so WireGuard stops dropping IPv6

_wireguard_up() sets allowed_ip_types[0.0.0.0/0] when the config declares an IPv4 Address, but the IPv6 branch never sets ::/0. The peer therefore only accepts the tunnel's own /128, while _routing_add() installs a default IPv6 route into the interface, so every outbound IPv6 packet is routed into WireGuard and dropped.

Regression from 7af8610a25; the pre-refactor code appended ::/0 in the same place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Update config.yaml

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 08:10:23 +02:00
Alexandre
57cbc726f6 docs(skill): terse chat replies in the add-on workflow (#2969)
Adds a short answer-style rule to the hassio-addon-workflow skill: no
pleasantries, no tool-call narration, no dumped logs, no re-printing what
is already in context. Uncertainty markers, negations, numbers and
verbatim technical text are explicitly exempt, so the Verified / Checked /
Assumed discipline in step 9 is not compressed away. Persisted text
(commits, CHANGELOG, PR bodies, review replies, the report) stays prose.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:42:36 +02:00
Alexandre
312c4cc949 Update updater.json 2026-08-12 19:58:40 +02:00
github-actions
eec248c9d0 GitHub bot : README updated 2026-08-12 17:19:26 +00:00
github-actions
a6c87588ad GitHub bot: changelog [nobuild] 2026-08-12 15:25:10 +00:00
Enrique
dbd74e0391 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>
2026-08-12 17:21:14 +02:00
github-actions
8d04564f94 Github bot : issues linked to readme 2026-08-12 12:33:50 +00:00
Alexandre
3af76ebb7a fix: wait for the Supervisor API before running add-on startup scripts (#2967)
* fix: wait for the Supervisor API before running startup scripts

48 add-ons build their nginx ingress config out of bashio::addon.ip_address
and bashio::addon.ingress_port. Both come from one GET /addons/self/info, and
when that is answered before the Supervisor is ready bashio prints nothing.
Nine add-ons paste the result straight into a sed and end up writing
"listen : default_server;", which nginx rejects with `invalid port in ":"`;
the other 39 assign first and abort under set -e, leaving %%port%%
placeholders. Either way ingress is dead for that boot.

ha_entrypoint.sh now polls /addons/self/info once before the cont-init loop
and waits until it reports this add-on's ip_address (and, for ingress
add-ons, a non-zero ingress_port). Bounded at 30s via HA_SUPERVISOR_WAIT,
never fatal, and skipped entirely without SUPERVISOR_TOKEN or curl. When the
Supervisor is already up -- the normal case -- it costs one request.

qBittorrent is bumped so the change is actually built and reaches the add-on
with the open report; the other add-ons pick it up on their next rebuild.

Refs #2949, #2962

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: harden the Supervisor wait after bot review

- HA_SUPERVISOR_WAIT=08 was accepted by test -gt but read as octal by
  arithmetic expansion, leaving deadline empty; the comparison then errored
  every iteration and the loop never exited, hanging start-up. Digits-only
  validation plus base-10 forcing.
- A request started near the deadline could run --max-time past it. The
  per-request timeout is now capped to the time remaining, and the retry
  sleep is skipped once the budget is gone, so the ceiling is exact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: probe the Supervisor through bashio instead of curl + sed

The wait reimplemented what the 48 consumers already do: it called
/addons/self/info with curl and picked the fields out with sed. That parallel
implementation was where one of the review findings landed, and it left a
residual race -- proving the API answered a moment ago says nothing about the
bashio call that runs next.

Probing through bashio removes both. bashio caches a successful
/addons/self/info under ${CACHE_DIR:-/tmp/.bashio}, so once the probe returns,
every bashio::addon.* call in every cont-init script reads that file rather
than asking the Supervisor again. Verified: one bashio::addon.ip_address call
writes a 26 KB addons.self.info.cache.

One call also settles all the fields, so the separate ingress/ingress_port
branch was redundant and is gone: a populated ip_address means the whole object
is cached. 36 -> 31 code lines.

Two consequences handled: bashio's own curl carries no --max-time (api.sh:41),
so each attempt is bounded with timeout; and bashio-standalone.sh answers these
calls from environment variables without ever contacting the Supervisor, so
BASHIO_LIB_FULL gates the probe to images carrying the real library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:33:28 +02:00
Alexandre
fa348c7051 Refactor addon build-args to use yq for YAML files
https://github.com/alexbelgium/hassio-addons/issues/2966
2026-08-12 14:29:41 +02:00
github-actions
ac205b7901 Github bot : issues linked to readme 2026-08-12 12:05:59 +00:00
Alexandre
295efe88e2 Update onpr_check-pr.yaml 2026-08-12 13:45:45 +02:00
Alexandre
4c7c1f99a4 Update onpush_builder.yaml 2026-08-12 13:45:39 +02:00
Alexandre
1e3ec448df Update build.json 2026-08-12 13:31:00 +02:00
github-actions
3e384fb068 GitHub bot: changelog [nobuild] 2026-08-12 09:09:39 +00:00
Alexandre
843a909366 Update config.yaml 2026-08-12 11:07:53 +02:00
Alexandre
c7d48baba1 Update config.yaml 2026-08-12 11:07:33 +02:00
Alexandre
c53342f2e8 fix(komga): rewrite the cookie path onto the ingress entry (#2964)
Komga scopes its cookies to its servlet context path, so the browser never
sent them back from the ingress url and every request after a successful
login was anonymous (401).

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:33:27 +02:00
Alexandre
bb1f7c302b fix(komga): ship an apparmor profile so local disks can be mounted (#2963)
* fix(komga): ship an apparmor profile so local disks can be mounted

Without apparmor.txt Supervisor adds no apparmor security_opt, so Docker's
default profile applies and denies mount() and raw block device access:
mount reported 'cannot mount /dev/sda1 read-only' and the kernel logged
'/dev/disk/by-label/NAS: Can't open blockdev'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-12 08:41:36 +02:00
Alexandre
cfcdd94ae6 fix(komga): bound the nginx readiness probes (#2961)
* fix(komga): bound the nginx readiness probes and log an exhausted wait

Follow-up to #2960, which merged one commit before this landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(komga): use a wall clock deadline for the readiness wait

An attempt count plus a per probe timeout stretched the wait to roughly twice
the advertised 15 minute ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:14:28 +02:00
github-actions
985f570263 Github bot : issues linked to readme 2026-08-12 04:01:32 +00:00
github-actions
e7a07b1d15 GitHub bot : README updated 2026-08-11 17:19:27 +00:00
Alexandre
44c58ba22b Update config.yaml nobuild 2026-08-11 18:59:23 +02:00
Alexandre
b417da3980 fix(komga): restore add-on reverted by a transient ghcr.io login failure (#2960)
* fix(komga): restore add-on reverted by a transient ghcr login failure

The amd64 builder job failed at docker login (denied: denied) before any build
step ran, which tripped revert-on-failure. Re-running the same commit unchanged
succeeded and both arch images are published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(komga): poll komga directly instead of bashio::net.wait_for, clarify config path

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:44:54 +02:00
76 changed files with 2647 additions and 15 deletions

View File

@@ -11,6 +11,16 @@ description: >-
# Home Assistant add-on workflow
**Answer style.** Chat replies are terse: no pleasantries, no tool-call narration, no decorative
tables or emoji, no dumped logs — quote the shortest decisive line, and don't re-read or re-print
what is already in context. Fragments and dropped articles are fine. Never compressed: uncertainty
markers ("likely", "assumed", "not verified"), negations (`not`/`never`/`no`/`only`), numbers,
units, technical terms, code blocks, error strings — step 9's Verified/Checked/Assumed distinction
outranks brevity every time. Write in full prose, not fragments, for security warnings,
irreversible-action confirmations, and any multi-step sequence a fragment could make ambiguous.
Persisted text is prose too: commits, CHANGELOG entries, PR bodies, review-thread replies, the
step 10 report.
Triage first, then one of two paths:
- **Light** — typo/doc fixes, CHANGELOG edits, version bumps, one-file edits at ladder levels

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

@@ -91,7 +91,7 @@ jobs:
uses: actions/checkout@v7.0.1
- name: 🔎 Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@f6bef06a4cee6c67924b0a70be643aacb8500a43
uses: frenck/action-addon-linter@v2
with:
path: "./${{ matrix.addon }}"
@@ -193,15 +193,15 @@ jobs:
- name: 💽 Create addon build-args
id: build_args
shell: bash
run: |
{
echo "armhf=BUILD_FROM=$(jq -r .build_from.armhf // empty ${{ steps.information.outputs.build }})"
echo "armv7=BUILD_FROM=$(jq -r .build_from.armv7 // empty ${{ steps.information.outputs.build }})"
echo "aarch64=BUILD_FROM=$(jq -r .build_from.aarch64 // empty ${{ steps.information.outputs.build }})"
echo "amd64=BUILD_FROM=$(jq -r .build_from.amd64 // empty ${{ steps.information.outputs.build }})"
echo "i386=BUILD_FROM=$(jq -r .build_from.i386 // empty ${{ steps.information.outputs.build }})"
} >> "$GITHUB_OUTPUT"
build_file="${{ steps.information.outputs.build }}"
case "$build_file" in
*.yaml|*.yml) build_json="$(yq -o=json '.' "$build_file")" ;;
*) build_json="$(cat "$build_file")" ;;
esac
for arch in armhf armv7 aarch64 amd64 i386; do
echo "${arch}=BUILD_FROM=$(printf '%s' "$build_json" | jq -r --arg a "$arch" '.build_from[$a] // empty')"
done >> "$GITHUB_OUTPUT"
- name: 🏗️ Set up QEMU
uses: docker/setup-qemu-action@v4

View File

@@ -114,7 +114,7 @@ jobs:
steps:
- uses: actions/checkout@v7.0.1
- name: Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@f6bef06a4cee6c67924b0a70be643aacb8500a43
uses: frenck/action-addon-linter@v2
with:
path: "./${{ matrix.addon }}"

View File

@@ -173,9 +173,13 @@ fi
####################################
BASHIO_LIB=""
BASHIO_LIB_FULL=false
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do
if [ -f "$f" ]; then
BASHIO_LIB="$f"
# The real library, which talks to the Supervisor. The standalone shim below only reads
# environment variables, which matters to wait_for_supervisor().
BASHIO_LIB_FULL=true
break
fi
done
@@ -188,6 +192,83 @@ if [ -z "$BASHIO_LIB" ]; then
done
fi
##############################
# Wait for the Supervisor API #
##############################
# Many cont-init scripts build their nginx ingress config out of bashio::addon.ip_address and
# bashio::addon.ingress_port. Both come from one GET /addons/self/info, and when that is answered
# before the Supervisor is ready bashio prints nothing: the add-on then either writes
# "listen : default_server;" -- which nginx rejects with `invalid port in ":"` -- or aborts under
# set -e and leaves the %%port%% placeholders in place. Either way the add-on cannot serve ingress.
# Ask for the same values here, through the same bashio calls, until they come back usable --
# rather than making 48 add-ons defend themselves against the same empty answer.
#
# Going through bashio rather than curl is what makes this reliable rather than merely likely:
# bashio caches a successful /addons/self/info under ${CACHE_DIR:-/tmp/.bashio}, so once this
# returns, every later bashio::addon.* call in every cont-init script reads that file instead of
# asking the Supervisor again. A probe that only proved the API was up a moment ago would leave
# the very next call free to fail.
#
# Bounded and never fatal: an add-on with no SUPERVISOR_TOKEN, or a Supervisor that stays
# unreachable, still has to start. HA_SUPERVISOR_WAIT (seconds, default 30) sets the ceiling; 0
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
wait_for_supervisor() {
local max="${HA_SUPERVISOR_WAIT:-30}"
local started deadline remaining attempt announced=0
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
# the probe and would burn the whole ceiling on every boot.
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
command -v timeout >/dev/null 2>&1 || return 0
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
# spin the loop below forever.
case "$max" in '' | *[!0-9]*) return 0 ;; esac
max=$((10#$max))
[ "$max" -gt 0 ] || return 0
started=$SECONDS
deadline=$((started + max))
while :; do
remaining=$((deadline - SECONDS))
if [ "$remaining" -le 0 ]; then
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
return 0
fi
# No single attempt may outlive the ceiling it is bounded by.
attempt=5
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
# logging is dropped because a failed attempt here is expected, not news.
# shellcheck disable=SC2016
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
_ "$BASHIO_LIB" >/dev/null 2>&1; then
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
return 0
fi
if [ "$announced" -eq 0 ]; then
echo "Waiting for the Supervisor API to report this add-on's network details..."
announced=1
fi
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
done
}
wait_for_supervisor
####################
# Starting scripts #
####################

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

@@ -56,7 +56,7 @@ If you want to do add the repository manually, please follow the procedure highl
### Number of addons
- In the repository : 137
- In the repository : 141
- Installed : 325872
### Top 3
@@ -567,6 +567,16 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
&#10003; ![image](https://api.iconify.design/mdi/book-open-page-variant.svg) [Komga](komga/) : Free and open source comics/mangas media server
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fkomga%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fkomga%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]
&#10003; ![image](https://api.iconify.design/mdi/speedometer.svg) [LibreSpeed](librespeed/) : A very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Flibrespeed%2Fconfig.yaml)
@@ -708,6 +718,27 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
&#10003; [Obsidian Sync Server](obsidian_syncserver_solo/) : Self-hosted Obsidian LiveSync backend on CouchDB. Plain HTTP — put your own reverse proxy in front for TLS.
&emsp;&emsp;![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)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_solo%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
&#10003; [Obsidian Sync Server NPM](obsidian_syncserver_npm/) : Self-hosted Obsidian LiveSync backend on CouchDB, bundled with Nginx Proxy Manager for TLS and certificate management.
&emsp;&emsp;![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)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_npm%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
&#10003; [Obsidian Sync Server SSL](obsidian_syncserver_ssl/) : Self-hosted Obsidian LiveSync backend on CouchDB, serving HTTPS with your own certificates from /ssl. Supports mobile Obsidian.
&emsp;&emsp;![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)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fobsidian_syncserver_ssl%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
&#10003; [Omada (obsolete)](zzz_archived_omada/) : TP-Link Omada Controller (obsolete, use https://github.com/jkunczik/home-assistant-omada)
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fzzz_archived_omada%2Fconfig.yaml)

View File

@@ -1,6 +1,6 @@
{
"source": "github",
"upstream_repo": "linuxserver/docker-baseimage-selkies",
"upstream_repo": "aaddrick/claude-desktop-debian",
"github_fulltag": true,
"slug": "claude_desktop",
"paused": false,

20
komga/CHANGELOG.md Normal file
View File

@@ -0,0 +1,20 @@
## 1.26.1.4 (12-08-2026)
- Minor bugs fixed
## 1.26.1.3 (2026-08-12)
- Fix : 401 errors after a successful login through ingress. Komga scopes its session cookies to its servlet context path (`Path=/komga`), which the browser never sends back from the ingress url, so every request after the login was anonymous. Nginx now rewrites the cookie path onto the ingress entry
## 1.26.1.2 (2026-08-12)
- Fix : local disks (`localdisks`) and SMB shares failed to mount with `cannot mount /dev/sdX read-only`. Without an `apparmor.txt` the add-on ran under Docker's default AppArmor profile, which denies `mount` and raw block device access. Ships the same profile as the other add-ons that mount disks
## 1.26.1.1 (2026-08-11)
- Bound the nginx readiness probes (`--connect-timeout` / `--max-time`) so a stalled connection cannot hang the wait, and log a warning when Komga has not answered within 15 minutes
## 1.26.1 (2026-08-11)
- Initial release, based on gotson/komga ([changelog](https://github.com/gotson/komga/releases))
- Ingress support : Komga is served on the `/komga` servlet context path, nginx prefixes it back with the ingress entry
- Supports local disks and SMB network shares for libraries (`localdisks` / `networkdisks` options)
- Supports extra environment variables via the `env_vars` option, see the [documentation](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2)

121
komga/Dockerfile Normal file
View File

@@ -0,0 +1,121 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
# _.------.
# _.-` ('>.-`"""-.
# '.--'` _'` _ .--.)
# -' '-.-';` `
# ' - _.' ``'--.
# '---` .-'""`
# /`
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM
ARG BUILD_VERSION
ARG BUILD_UPSTREAM="1.26.1"
FROM ${BUILD_FROM}
ENV BASHIO_VERSION=0.14.3
##################
# 2 Modify Image #
##################
# No S6_* tuning here : the upstream image ships no s6-overlay, so the vars the
# other addons set would be read by nobody
# Komga is served from a fixed servlet context path. Ingress strips its own
# prefix before forwarding, and Komga renders absolute asset urls, so nginx
# needs a stable subpath to prefix back. See rootfs/etc/nginx/servers/ingress.conf
ENV SERVER_SERVLET_CONTEXTPATH="/komga"
##################
# 3 Install apps #
##################
# Add rootfs
# Absolute paths on purpose : the upstream image sets WORKDIR /app, so the
# relative "find ." used by the other addons would miss /etc entirely
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/services.d -type f \( -name "*.sh" -o -name "run" \) -print -exec chmod +x {} \;
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
# Modules
ARG MODULES="00-banner.sh 00-global_var.sh 01-custom_script.sh 00-local_mounts.sh 00-smb_mounts.sh"
# Automatic modules download
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="nginx"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
################
# 4 Entrypoint #
################
# The upstream image is a plain jre image without s6-overlay, so ha_entrypoint
# runs as pid 1 : it executes /etc/cont-init.d, then supervises /etc/services.d
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
ENTRYPOINT ["/ha_entrypoint.sh"]
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
# Komga is a jvm app, first boot builds the database : leave it time to settle
ENV HEALTH_PORT="25600" \
HEALTH_URL="/komga/"
HEALTHCHECK \
--interval=30s \
--retries=5 \
--start-period=180s \
--timeout=25s \
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" >/dev/null 2>&1 || exit 1

70
komga/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Home Assistant Add-on: Komga
Free and open source comics/mangas media server.
[Komga](https://komga.org) organizes your comics, mangas, BDs, magazines and ebooks, serves them
through a web reader, and exposes OPDS, Kobo sync and a REST API for third-party readers
(Tachiyomi/Mihon, Panels, Chunky, ...).
## About
- Browse and read CBZ, CBR, PDF and EPUB files from any browser
- Import metadata, edit series/books, build collections and read lists
- Multi-user, with per-user library restrictions and age ratings
- OPDS v1/v2, Kobo sync, and a documented REST API
## Installation
1. Add this repository to Home Assistant.
2. Install the **Komga** add-on.
3. Start the add-on and open it from the sidebar (ingress), or on port `25600` at
`http://homeassistant:25600/komga`.
4. Create the initial user account when the web interface asks for it.
5. Add a library pointing at your comics, for example `/media/comics` or `/share/comics`.
The first start takes longer than usual: Komga is a JVM application and builds its database and
search index on first boot.
## Configuration
| Option | Description |
|--------|-------------|
| `PUID` / `PGID` | Ownership applied to the add-on configuration directory. Defaults to `0` (root). |
| `TZ` | Timezone, e.g. `Europe/Paris`. |
| `localdisks` | Local disks to mount, e.g. `sda1` or a disk label. |
| `networkdisks` | SMB shares to mount, e.g. `//192.168.1.2/comics`. Mounted under `/mnt`. |
| `cifsusername` / `cifspassword` / `cifsdomain` | Credentials for the SMB shares. |
| `smbv1` | Allow the legacy SMBv1 protocol. |
| `env_vars` | Extra environment variables passed to Komga. See the [wiki](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2). |
Most Komga settings can be passed through `env_vars` using the upstream naming, see the
[Komga configuration options](https://komga.org/docs/installation/configuration/). A common one:
- `JAVA_TOOL_OPTIONS` = `-Xmx1g` — cap the JVM heap on small machines.
`SERVER_SERVLET_CONTEXTPATH` and `SERVER_PORT` are reserved by the add-on: ingress is built
around the `/komga` path on port 25600, and overriding either breaks the sidebar panel.
## Ingress and URLs
Komga is served from the `/komga` subpath so that it works behind Home Assistant ingress:
- from the Home Assistant sidebar: ingress, no extra setup
- directly: `http://homeassistant:25600/komga`
External clients — OPDS readers, Kobo sync, Tachiyomi/Mihon, Panels — must use the direct
`http://homeassistant:25600/komga` url. Ingress is browser-session based, so those clients cannot
authenticate through it.
## Data
Komga's database, logs and search index live in `/config` inside the add-on, which Home Assistant
maps to this add-on's own configuration directory — `/addon_configs/<repository_id>_komga`, browsable
with the Filebrowser add-on. They survive add-on updates. Libraries stay where you put them, under
`/media`, `/share` or a mounted disk.
## Support
- [Komga upstream project](https://github.com/gotson/komga)
- [Add-on repository issues](https://github.com/alexbelgium/hassio-addons/issues)

68
komga/apparmor.txt Normal file
View File

@@ -0,0 +1,68 @@
#include <tunables/global>
profile komga_addon flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
capability chown,
capability dac_override,
capability dac_read_search,
capability fowner,
capability setgid,
capability setuid,
capability sys_chroot,
capability sys_admin,
file,
signal,
mount,
umount,
remount,
network udp,
network tcp,
network dgram,
network stream,
network inet,
network inet6,
network netlink raw,
network unix dgram,
# Entrypoint stack
/init ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/run/{,**} rwk,
/dev/tty rw,
/bin/** ix,
/usr/bin/** ix,
/usr/lib/bashio/** ix,
/etc/s6/** rix,
/run/s6/** rix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/init rix,
/var/run/** mrwkl,
/var/run/ mrwkl,
/dev/i2c-1 mrwkl,
# Files required
/dev/fuse mrwkl,
/dev/sda1 mrwkl,
/dev/sdb1 mrwkl,
/dev/nvme0 mrwkl,
/dev/nvme1 mrwkl,
/dev/mmcblk0p1 mrwkl,
/dev/* mrwkl,
/tmp/** mrkwl,
# Data access
/data/** rw,
# suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
ptrace (trace,read) peer=docker-default,
# docker daemon confinement requires explicit allow rule for signal
signal (receive) set=(kill,term) peer=/usr/bin/docker,
}

6
komga/build.json Normal file
View File

@@ -0,0 +1,6 @@
{
"build_from": {
"aarch64": "gotson/komga:latest",
"amd64": "gotson/komga:latest"
}
}

104
komga/config.yaml Normal file
View File

@@ -0,0 +1,104 @@
arch:
- aarch64
- amd64
description: Free and open source comics/mangas media server
devices:
- /dev/dri
- /dev/dri/card0
- /dev/dri/card1
- /dev/dri/renderD128
- /dev/vchiq
- /dev/video10
- /dev/video11
- /dev/video12
- /dev/video13
- /dev/video14
- /dev/video15
- /dev/video16
- /dev/ttyUSB0
- /dev/sda
- /dev/sdb
- /dev/sdc
- /dev/sdd
- /dev/sde
- /dev/sdf
- /dev/sdg
- /dev/nvme
- /dev/nvme0
- /dev/nvme0n1
- /dev/nvme0n1p1
- /dev/nvme0n1p2
- /dev/nvme0n1p3
- /dev/nvme1n1
- /dev/nvme1n1p1
- /dev/nvme1n1p2
- /dev/nvme1n1p3
- /dev/nvme2n1
- /dev/nvme2n1p1
- /dev/nvme2n1p2
- /dev/nvme2n3p3
- /dev/mmcblk
- /dev/fuse
- /dev/sda1
- /dev/sdb1
- /dev/sdc1
- /dev/sdd1
- /dev/sde1
- /dev/sdf1
- /dev/sdg1
- /dev/sda2
- /dev/sdb2
- /dev/sdc2
- /dev/sdd2
- /dev/sde2
- /dev/sdf2
- /dev/sdg2
- /dev/sda3
- /dev/sdb3
- /dev/sda4
- /dev/sdb4
- /dev/sda5
- /dev/sda6
- /dev/sda7
- /dev/sda8
- /dev/nvme0
- /dev/nvme1
- /dev/nvme2
image: ghcr.io/alexbelgium/komga-{arch}
ingress: true
ingress_entry: komga/next
init: false
map:
- addon_config:rw
- media:rw
- share:rw
name: Komga
options:
env_vars: []
PGID: 0
PUID: 0
panel_icon: mdi:book-open-page-variant
ports:
25600/tcp: 25600
ports_description:
25600/tcp: Web interface (path /komga)
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
PGID: int
PUID: int
TZ: str?
cifsdomain: str?
cifspassword: str?
cifsusername: str?
localdisks: str?
networkdisks: str?
smbv1: bool?
slug: komga
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/komga
version: "1.26.1.4"

BIN
komga/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
komga/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,15 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Komga stores its database, logs and search index in KOMGA_CONFIGDIR, which the
# upstream image sets to /config -- that is the addon_config mount
CONFIG_LOCATION="/config"
bashio::log.info "Config stored in $CONFIG_LOCATION"
mkdir -p "$CONFIG_LOCATION"
# Numbered 20- on purpose : it must sort after 00-global_var.sh, which is what
# exports PUID/PGID from the addon options. The upstream image sets neither, so
# the fallbacks only apply when the module is absent.
chown -R "${PUID:-0}:${PGID:-0}" "$CONFIG_LOCATION"

View File

@@ -0,0 +1,17 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
#################
# NGINX SETTING #
#################
declare ingress_interface
declare ingress_port
declare ingress_entry
ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf
sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf

View File

@@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -0,0 +1,56 @@
# Run nginx in foreground.
daemon off;
# This is run inside Docker.
user root;
# Pid storage location.
pid /var/run/nginx.pid;
# Set number of worker processes.
worker_processes 1;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Write error log to Hass.io add-on log.
error_log /proc/1/fd/1 error;
# Load allowed environment vars
env HASSIO_TOKEN;
# Load dynamic modules.
include /etc/nginx/modules-enabled/*.conf;
# Max num of simultaneous connections by a worker process.
events {
worker_connections 512;
}
http {
include /etc/nginx/includes/mime.types;
log_format hassio '[$time_local] $status '
'$http_x_forwarded_for($remote_addr) '
'$request ($http_user_agent)';
access_log /proc/1/fd/1 hassio;
client_max_body_size 4G;
default_type application/octet-stream;
gzip on;
keepalive_timeout 65;
sendfile on;
server_tokens off;
tcp_nodelay on;
tcp_nopush on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/servers/*.conf;
}

View File

@@ -0,0 +1,67 @@
server {
listen %%interface%%:%%port%% default_server;
client_max_body_size 0;
# Home Assistant opens the ingress panel at <ingress_entry>/ and forwards it
# as / , but Komga only answers below its servlet context path (/komga), so
# bounce the panel there. absolute_redirect off keeps the Location relative
# to the HA host instead of nginx's own listen address.
location = / {
absolute_redirect off;
return 302 %%ingress_entry%%/komga/;
}
location / {
add_header Access-Control-Allow-Origin *;
proxy_connect_timeout 30m;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_pass http://127.0.0.1:25600;
# Komga pushes live events over SSE (/komga/sse/v1/events), which must
# not be buffered or the UI stops refreshing until the buffer fills
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Spring redirects /komga to /komga/ ; the Location it produces is
# absolute against the upstream address, so rewrite it back onto the
# ingress path (the second rule covers an already relative Location).
absolute_redirect off;
proxy_redirect http://127.0.0.1:25600/ %%ingress_entry%%/;
proxy_redirect / %%ingress_entry%%/;
# Komga scopes its cookies to the servlet context path
# (Set-Cookie: ...; Path=/komga). The browser lives under the ingress
# entry, so such a cookie is never sent back : login succeeds, then
# every following request arrives anonymous and Komga answers 401.
proxy_cookie_path /komga %%ingress_entry%%/komga;
proxy_cookie_path / %%ingress_entry%%/;
# Komga renders its index page with Thymeleaf @{...} link expressions,
# so every asset url and window.resourceBaseUrl carry the context path
# (/komga). Ingress strips its own prefix before forwarding, so the
# browser needs that prefix added back. Only text/html is rewritten
# (the nginx default for sub_filter_types) : the SPA derives its api
# origin and router base from resourceBaseUrl at runtime, so json
# responses and book pages stream through untouched.
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter "/komga" "%%ingress_entry%%/komga";
# The epub/divina reader fetches a Readium manifest whose links Komga
# builds with ServletUriComponentsBuilder.fromCurrentContextPath(), so
# they are fully absolute against the upstream address nginx talks to
# (http://127.0.0.1:25600/komga). Rewriting them to a root relative
# ingress path also fixes the scheme : Home Assistant may be served over
# https, and an absolute http:// link would be blocked as mixed content.
# Only the json/xml document types are added here, so book pages are
# never scanned.
sub_filter "http://127.0.0.1:25600/komga" "%%ingress_entry%%/komga";
sub_filter_types application/json application/webpub+json
application/divina+json application/opds+json
application/atom+xml;
}
}

View File

@@ -0,0 +1,17 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Same invocation as the upstream image entrypoint (gotson/komga), which is
# replaced by ha_entrypoint.sh so that cont-init.d and nginx can run too.
# SERVER_SERVLET_CONTEXTPATH is set in the Dockerfile, see ingress.conf.
bashio::log.info "Starting Komga (served on the /komga path, see the addon documentation)"
cd /app
exec java \
-Dspring.profiles.include=docker \
--enable-native-access=ALL-UNNAMED \
-jar application.jar \
--spring.config.additional-location=file:/config/

View File

@@ -0,0 +1,34 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Wait for komga to answer before nginx starts serving ingress. First boot
# builds the database, so leave a wide margin, but poll rather than call
# bashio::net.wait_for : bashio takes (port host timeout) while the bundled
# bashio-standalone.sh takes (host port timeout), and picking the wrong one
# would either fail instantly or block for the whole timeout.
# The per probe timeouts keep the 15 minute ceiling real : without them a
# half open connection would hang a single probe, and the loop, forever.
# A wall clock deadline, not an attempt count : a failed probe costs up to
# max-time on top of the sleep, so counting attempts would stretch the wait to
# roughly twice the advertised ceiling.
komga_ready=false
deadline=$((SECONDS + 900))
while [ "$SECONDS" -lt "$deadline" ]; do
if curl -sf --connect-timeout 2 --max-time 5 -o /dev/null "http://127.0.0.1:25600/komga/"; then
komga_ready=true
break
fi
sleep 5
done
# Deliberately not fatal : nginx serving a 502 tells the user something is wrong
# and starts working by itself once komga finally answers, while refusing to
# start would take ingress down for good after ha_entrypoint gives up retrying.
if [ "$komga_ready" != true ]; then
bashio::log.warning "Komga did not answer within 15 minutes. Starting NGinx anyway : ingress will return 502 until it does."
fi
bashio::log.info "Starting NGinx..."
exec nginx

9
komga/updater.json Normal file
View File

@@ -0,0 +1,9 @@
{
"github_beta": "false",
"last_update": "2026-08-11",
"repository": "alexbelgium/hassio-addons",
"slug": "komga",
"source": "github",
"upstream_repo": "gotson/komga",
"upstream_version": "1.26.1"
}

View File

@@ -0,0 +1,19 @@
## 3.5.2.1 (12-08-2026)
- Minor bugs fixed
# 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,13 @@
## 3.5.2 (12-08-2026)
- Minor bugs fixed
# 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,15 @@
## 3.5.2 (12-08-2026)
- Minor bugs fixed
# 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"
}

View File

@@ -1,4 +1,12 @@
## 5.2.3.3 (2026-08-13)
- Fix : with WireGuard, IPv6 traffic was silently dropped. When the WireGuard config declared an IPv6 `Address` (Mullvad, AirVPN and ProtonVPN all do), `vpn` added a default IPv6 route into the tunnel but never added `::/0` to the peer's `allowed-ips`, so WireGuard discarded every outgoing IPv6 packet. `::/0` is now allowed for IPv6 tunnels, mirroring the existing `0.0.0.0/0` handling for IPv4
## 5.2.3.2 (2026-08-12)
- Fix : ingress could fail permanently with `nginx: [emerg] invalid port in ":"`. `30-nginx.sh` pastes `bashio::addon.ip_address` and `bashio::addon.ingress_port` straight into the nginx config; when the Supervisor answers before it is ready both come back empty and the config gets `listen : default_server;`. The add-on entrypoint now waits (up to 30s, tunable with `HA_SUPERVISOR_WAIT`) for the Supervisor to report the add-on's network details before any startup script runs
## 5.2.3-1 (2026-07-09)
- Rebuild images after VueTorrent download path fix

View File

@@ -143,4 +143,4 @@ schema:
slug: qbittorrent
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "5.2.3.1"
version: "5.2.3.3"

View File

@@ -293,6 +293,7 @@ _wireguard_up() {
elif [ "${result}" -eq 2 ]; then
config["IPv6Enabled"]="true"
local_ip_types["${local_ip}"]="ipv6"
allowed_ip_types["::/0"]="ipv6"
_cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
else
bashio::log.warning "Ignoring invalid local IP address: ${local_ip}"

View File

@@ -1,4 +1,3 @@
## &#9888; Open Request : [✨ [REQUEST] qbittorrent (opened 2025-10-11)](https://github.com/alexbelgium/hassio-addons/issues/2139) by [@nyok92](https://github.com/nyok92)
## &#9888; Open Request : [✨ [REQUEST] transmission, document where torrent files are stored (opened 2026-07-14)](https://github.com/alexbelgium/hassio-addons/issues/2852) by [@bilogic](https://github.com/bilogic)
# Hass.io Add-ons: Tor with bridges