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
.mdnotes. - 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.
1. SilverBullet — breaks Obsidian's short wikilinks, by design
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
:arm64image's desktop backend (xrdp + Guacamole, an s6-supervised stack) wouldn't start on a Raspberry Pi: the mainxrdpdaemon never listened on 3389, with repeatings6-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
stablechannel 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-privateAndroid/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, largemax_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
.stignorethe LiveSync plugin config so it doesn't propagate to your Syncthing devices. I chose the desktop bridge for robustness.
- 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
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> deletethen... 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. .stignoreto 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
couchdbDocker image runs as uid 5984 —chown -R 5984:5984your mounteddata/etcdirs or it can't write. - Enable
single_node=trueso CouchDB creates_users/_replicatoron 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_countwon't move until that finishes and replication begins. Be patient and watch the remote DB'sdoc_count/size to confirm upload.
nginx reverse proxy
- After
nginx -t && systemctl reload nginx, a freshly addedlocationcan 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), useproxy_passwithout a trailing slash. - CouchDB + LiveSync want a large/unlimited
client_max_body_sizeand 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 watchworkspace.jsonchurn) or.stignoreit (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.
留言