Moving off OneDrive onto a Syncthing mesh
I had been sync'ing my ~/code folder through OneDrive for years. It worked
until it didn't. The breaking point was Xcode and git both fighting OneDrive
for ownership of the same files. I moved the whole mesh off OneDrive and
onto Syncthing.
What was happening
OneDrive's File Provider on macOS is opinionated. It mangles filenames, replaces large files with zero-byte placeholders that have to be "downloaded" on access, and rate-limits the upload API in ways that make git operations on a big repo feel like they're happening over a dial-up modem.
Specific pain points by the end:
- Xcode builds occasionally read a placeholder instead of the actual file and produce nonsense errors
- Git operations on
~/codewere 2–10x slower than the same operations on any non-OneDrive directory - Spotlight kept reindexing because OneDrive kept marking files dirty
- I had filename collisions on cross-platform paths because OneDrive doesn't allow some characters Windows is also fine with
What I found
The actual requirement is "the same ~/code directory on four machines,
with a 30-day local history, conflicts resolved deterministically." There
is no requirement for cloud storage. Cloud storage was just the
transport I picked years ago.
Syncthing is the right transport for this: LAN-only, no API rate limits, deterministic conflict files named by device, configurable per-folder ignore patterns. The trade-off is no offsite copy by default — every peer is in my house.
The fix
The mesh ended up looking like this:
- The NAS as canonical (
/volume1/code) - An LXC at
/opt/<service>/data/code - A Mac mini at
/Users/<user>/code - A laptop at
C:\Users\<user>\code
All four peers sync a single folder ID. Type sendreceive,
ignorePerms=true, staggered versioning at 30 days. Each peer keeps
its own local history.
Two gotchas worth writing down:
Inotify limits on the NAS. A 165k-file folder eats inotify watches.
The default Linux limits are too low. Drop a file at
/etc/sysctl.d/99-syncthing.conf:
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 1024
Then sysctl --system and Rescan from the Syncthing UI. The "watcher
failed" warning clears within about a minute. Be aware that DSM major
upgrades may regenerate /etc/sysctl.conf and revert this, so keep a
note.
An ignore file for git repos. Syncthing will happily sync .git/
between peers and produce sync-conflict files inside .git/:
.git/index.sync-conflict-20260506-031312-PEGASUS
.git/AUTO_MERGE.sync-conflict-...
.git/logs/HEAD.sync-conflict-...
These corrupt git state silently. The fix is a .stignore at the
folder root, identical on all peers:
**/.git
**/.git/**
**/*sync-conflict-*
**/__pycache__
**/__pycache__/**
**/node_modules
**/node_modules/**
**/.next
**/.venv
**/venv
**/build
**/dist
**/.DS_Store
**/*.pyc
#recycle
@eaDir
**/@eaDir
**/@eaDir/**
**/Thumbs.db
**/desktop.ini
Apply via the REST API rather than editing the file by hand on every peer:
POST http://<peer>:8384/rest/db/ignores?folder=<folder-id>
X-API-Key: <key>
{"ignore": ["<line>", "<line>", ...]}
PowerShell quirk: Get-Content returns rich PSObjects, not strings.
You have to cast to [string[]] before ConvertTo-Json, or the
serializer will helpfully include PSPath / PSDrive metadata and
Syncthing will reject the payload. Also write the JSON without a BOM
or Syncthing's JSON parser chokes:
$lines = [string[]][System.IO.File]::ReadAllLines($path)
$json = $lines | ConvertTo-Json
[System.IO.File]::WriteAllText(
$out, $json,
(New-Object System.Text.UTF8Encoding $false))
What I'd do differently
I should have set up an offsite copy as a separate step, before ripping out the cloud transport. "Cloud was also my offsite" was an accidental property of the old setup that I didn't replace in the new one for a few weeks. The fix was a nightly packer pushing content-addressed blobs to a personal cloud bucket — same as the backup post — but I shouldn't have had a window where the only copies of my code were four machines on one LAN.
The other thing: .stignore first, sync second. Always. If you let
Syncthing index a folder full of .git/ directories before the ignore
is in place, you'll be cleaning up sync-conflict files inside .git
for a week.