Self-hosting multi-device Obsidian sync (Windows + Raspberry Pi + Android + iPhone): what worked, what didn't, and why

Self-hosting multi-device Obsidian sync (Windows + Raspberry Pi + Android + iPhone): what worked, what didn't, and why

A point-in-time field report (2026-06) on getting one Obsidian vault synced, self-hosted and free, across a Windows desktop, an always-on Raspberry Pi, three Android phones and an iPhone.

This is not another happy-path tutorial — the official docs for each tool are excellent. It's the part that's usually missing: the dead ends, the why-not, and the platform-specific traps (especially on ARM Raspberry Pi and on iOS). If you're about to do the same thing, this should save you a few evenings.

Author: fifthadj. Use freely. No warranty — tools move fast; treat this as a snapshot, not a maintained guide.


TL;DR — the setup that actually worked

Device Sync method
Windows desktop Syncthing (also doubles as the iOS "bridge", see below)
Raspberry Pi 4 (always-on Linux hub) Syncthing hub + CouchDB (for iOS)
Android phones (×3) Obsidian app + Syncthing-Fork
iPhone Obsidian app + Self-hosted LiveSync ↔︎ CouchDB

Two sync systems coexist on purpose:

  • Desktop + Android → Syncthing. Peer-to-peer, real-time, bidirectional, private (over a mesh VPN). Android's Obsidian can "Open folder as vault", so it opens the Syncthing-synced folder directly.
  • iPhone → obsidian-livesync (CouchDB). Because iOS Obsidian cannot open an external folder (sandbox), the Syncthing-into-a-folder trick is a dead end on iOS. LiveSync syncs inside the app via a database instead.

The single most important lesson: the only thing that looks and behaves like Obsidian is Obsidian itself. Every "view your vault in a browser" shortcut cost more than it saved.


The goal & constraints

  • One Obsidian vault, ~1.4 GB / ~4,300 files — but mostly images (~3,000 PNG/JPG, imported from OneNote); only ~1,100 are actual .md notes.
  • Devices: 1 Windows PC, 1 Raspberry Pi 4 (arm64, the always-on host), 3 Android phones, 1 iPhone.
  • Requirements: self-hosted, free, private (everything over a mesh VPN — I used Tailscale), and ideally real-time bidirectional.

What did NOT work (and why) — read this first

I tried to give phones/browsers access via a "web app" three different ways before accepting the native-app answer. All three are removed now. Here's why, so you don't repeat them.

SilverBullet is a lovely self-hosted, editable, markdown PWA. But Obsidian uses flat wikilinks[[note-name]] with no folder path — and resolves them by searching the whole vault. SilverBullet resolves [[name]] as an absolute page from the space root. So in a vault with folders, every short wikilink is broken: it points at root, the page isn't there, you get a blank/"create new page".

There is no setting to fix this. The community workaround is a script that rewrites every link to a full path — i.e. you mutate your whole vault. Not worth it. (SilverBullet is great if your vault was built in SilverBullet; it's a poor fit for an existing Obsidian vault.)

2. obsidian-remote — the right idea, broken on ARM

obsidian-remote runs the real Obsidian desktop in a container and streams it to your browser. That genuinely fixes the compatibility problem (it is Obsidian). But:

  • The :arm64 image's desktop backend (xrdp + Guacamole, an s6-supervised stack) wouldn't start on a Raspberry Pi: the main xrdp daemon never listened on 3389, with repeating s6-svwait: ... xrdp-sesman: No such file or directory. The web frontend served fine; the desktop session never came up.
  • Even if it had worked, streaming an Electron desktop 24/7 from a Pi is heavy and the touch-via-VNC UX on a phone is poor.

Also worth knowing: bumping a container's /dev/shm (default 64 MB) with --shm-size=1g is required for Electron/Chromium apps in Docker — but it didn't save this one.

3. Perlite — works, but it's a read-only viewer

Perlite is a PHP web viewer built for Obsidian vaults, so it correctly resolves short wikilinks (verified: [[001_foo]] rendered as /folder/subfolder/001_foo, is-unresolved: 0) and it's server-rendered (fast — no pushing 1.4 GB into the browser). It's the right tool if read-only browsing is all you need. For me it wasn't (I wanted to edit), and the default theme didn't win anyone over. Honorable mention, wrong job.

ARM note: the common sec77/perlite Docker image is amd64 + armv7 only — no arm64. On a 64-bit Pi, deploy it as plain PHP on your existing nginx + PHP-FPM instead (it's just PHP files + a vault folder).

Conclusion from all three: a heavily Obsidian-structured vault (short wikilinks + folders + lots of attachments) does not survive a lightweight web clone, and "real Obsidian in a browser" is too fragile on ARM. Put real Obsidian on each device.


What worked

Desktop + Android: Syncthing

  • Install Syncthing on the desktop and on the always-on Pi; pair them; share the vault folder as send-receive (bidirectional).
  • Mixed versions are fine. I ended up with Syncthing v2.1.x on Windows (winget) and v1.30 on the Pi (the official apt repo's stable channel still served 1.x). Syncthing guarantees v2 ↔︎ v1.27+ wire compatibility, so don't fight to unify them.
  • Connectivity: on the same LAN they connect directly via local discovery; off-LAN, hardcode each peer's mesh-VPN address (tcp://<vpn-ip>:22000) so it works anywhere. I disabled global discovery + relays so the notes never touch Syncthing's public infrastructure.
  • Safety nets worth turning on: staggered file versioning (per-file undo, in .stversions/) and a sane .stignore (see below).
  • Android: install Syncthing-Fork (Catfriend1 — the maintained fork; the original Android app is discontinued) + the Obsidian app. Sync the vault to a normal folder like /storage/emulated/0/Obsidian/Vault (not the app-private Android/data/... — Obsidian can't read another app's sandbox), then in Obsidian use "Open folder as vault" on it.

iPhone: obsidian-livesync + CouchDB (because iOS is different)

iOS Obsidian cannot open an arbitrary external folder — it only stores vaults in iCloud or its own app container. So a Syncthing-synced folder is unreachable to it (this also kills the "use Möbius Sync / SyncTrain on iOS" idea — they sync files Obsidian still can't open). The self-hosted answer is obsidian-livesync, which syncs inside Obsidian via a CouchDB database.

  • Run CouchDB (couchdb:3) on the always-on host (Docker; the image is multi-arch incl. arm64).
  • Apply the LiveSync-required CouchDB config (single-node, CORS for app://obsidian.md + capacitor://localhost, require_valid_user, large max_http_request_size / max_document_size).
  • Expose it over HTTPS behind a reverse proxy on your mesh VPN.
  • The bridge: LiveSync is an Obsidian plugin, not a server — so something running Obsidian + LiveSync has to feed the existing (Syncthing-synced) vault into CouchDB. The simplest reliable choice is your desktop's Obsidian with the LiveSync plugin. It bridges "Syncthing files ↔︎ CouchDB" whenever it's open.
    • Trade-off, stated honestly: changes from the iPhone reach the Android/desktop world only when the desktop Obsidian is open (the iPhone ↔︎ CouchDB link itself is always live). Running the bridge on the always-on Pi instead would need a headless Obsidian (xvfb + a GUI app as a 24/7 daemon) — doable but the most fragile component, plus it forces you to .stignore the LiveSync plugin config so it doesn't propagate to your Syncthing devices. I chose the desktop bridge for robustness.

Gotchas / troubleshooting (the actually-useful part)

Syncthing

  • "Connects, then drops, reconnects every ~20 s, transfers nothing" on Android → it's battery optimization killing the app. Set Syncthing-Fork to Unrestricted battery, keep it running in the background. This was the single biggest Android time-sink.
  • "Device connected but the folder shows 0 % / no transfer"accepting a device ≠ accepting a folder. They're two separate actions. If the folder-share prompt never appears (even with notifications enabled), re-trigger it from the server: remove the device from the folder's share list and re-add it (syncthing cli config folders <id> devices <DEVICE_ID> delete then ... devices add --device-id <ID>), or open the device's Web GUI and accept the yellow "wants to share" banner there.
  • A new device must actually be on the mesh VPN (logged in / connected), not just "installed".
  • Sync conflicts that are byte-different but identical line counts are almost always CRLF vs LF — cosmetic, content identical. Verify (strip \r, compare) and delete the .sync-conflict-* copies; they're harmless.
  • .stignore to stop per-machine churn: ignore Obsidian's per-device UI state and OS junk, e.g.:
    (?d).DS_Store
    (?d)Thumbs.db
    (?d)desktop.ini
    /.trash
    /.obsidian/workspace.json
    /.obsidian/workspace-mobile.json
    /.obsidian/cache
  • Big vault on a phone? Most of my 1.4 GB was images. To keep a phone text-only, set Syncthing-Fork ignore patterns (*.png, *.jpg, _resources, …) on that device before the first sync, or use LiveSync's "Maximum file size" on iOS. (Ignore patterns are per-device, so other devices keep the images.)

CouchDB + LiveSync

  • The couchdb Docker image runs as uid 5984chown -R 5984:5984 your mounted data/etc dirs or it can't write.
  • Enable single_node=true so CouchDB creates _users/_replicator on first boot.
  • LiveSync setup order is a footgun: the device that has the data (desktop) configures first and pushes/initializes the remote; the empty device (iPhone) configures second and fetches. If the empty device pushes, it overwrites the remote with nothing.
  • First device = manual setup. The "Setup URI" (a obsidian://setuplivesync?settings=... string + its passphrase) is something you generate on device #1 to import on device #2 (paste, or scan as a QR). Don't paste your CouchDB URL into the "Setup URI" field — that throws "Setup-URI not valid".
  • LiveSync's initial "STORAGE → DB" phase is local (reading files into the local DB); the remote doc_count won't move until that finishes and replication begins. Be patient and watch the remote DB's doc_count/size to confirm upload.

nginx reverse proxy

  • After nginx -t && systemctl reload nginx, a freshly added location can briefly 404 while old workers drain — wait a few seconds before concluding you misconfigured it.
  • For an app under a subpath that doesn't rewrite (e.g. CouchDB at /couchdb/), proxy_pass http://127.0.0.1:5984/; with a trailing slash strips the prefix. For an app that expects to see its prefix (set its own base-path/URL-prefix), use proxy_pass without a trailing slash.
  • CouchDB + LiveSync want a large/unlimited client_max_body_size and WebSocket-friendly proxying.

Cross-platform editing traps

  • Editing Linux config files (.sh, .ini, systemd units) from a Windows editor can write CRLF and break shebangs/parsing — edit on the Linux side or force LF.
  • Running an Obsidian instance per device means each gets its own .obsidian/ — decide whether to sync it (shared theme/plugins, but watch workspace.json churn) or .stignore it (independent per device).

Would I do it again?

For most people:

  • Desktop + Android: Syncthing + the Obsidian app is excellent, free, private, real-time. Just remember the battery-optimization gotcha.
  • iPhone is the hard part. Self-hosted = obsidian-livesync + CouchDB (with the bridge caveat above). If you don't mind paying, the official Obsidian Sync is far less hassle on iOS. iCloud only really shines if you live in the Apple ecosystem with a Mac.
  • Don't try to make a lightweight web app stand in for Obsidian on an existing Obsidian vault. Put real Obsidian on the device.

The honest summary: the tools aren't novel — the value was in which paths are dead ends and why, and the ARM/iOS specifics. Hopefully this list saves you the detours it cost me.

留言

這個網誌中的熱門文章

架站資源整理清單

這次多機聯防的程式筆記