The repo is nearly 40GB, and Pods, node_modules, and DerivedData combined outweigh the actual source code. You open Xcode locally, with the project mounted on an SMB share hosted on a remote cloud Mac, and indexing takes over a minute — switching branches stalls long enough that you start wondering if your network dropped. This is the first wall most people hit after moving their dev environment to a cloud Mac. Mounting a shared volume looks like the easy option, but in practice you quickly discover that large iOS projects and network filesystems are fundamentally at odds in how they access files. This post walks through how we used Mutagen to set up bidirectional sync between local and remote on an HireVPS node, bringing the experience back to something close to native.
Why mounting a shared volume doesn't work for large iOS projects
Network filesystems like NFS and SMB are designed around the assumption of "occasional access to large files." Xcode indexing, SourceKit, and CocoaPods cache scanning do the exact opposite: they stat, open, and close tens of thousands of small files in a short burst. Every single operation requires a network round trip — even at 20ms latency, multiply that by tens of thousands of calls and you're looking at minutes of lag. Worse, connection handling is fragile: the mount point can freeze entirely when the connection hiccups, taking down both your terminal and editor with it, leaving you no choice but to force-unmount and remount.
A side-by-side comparison of three common approaches makes this clearer:
| Approach | Real-time behavior | Handling many small files | Recovery from disconnects | Best suited for |
|---|---|---|---|---|
| SMB/NFS mount | Strong (always current) | Poor — per-file network round trips | Poor — prone to freezing | Occasional reads/writes of large files |
| Scheduled rsync | Weak — sync gaps exist | Decent — efficient batch transfers | Good — just rerun the job | Periodic backups, one-way pushes |
| Mutagen bidirectional sync | Near real-time | Good — local caching + incremental transfer | Good — auto-reconnect and resume | Ongoing bidirectional collaboration |
One lesson we learned the hard way: we initially took the shortcut of just mounting the share, and ended up with Xcode's "Building workspace" step taking an extra 15-25 seconds on average. Multiply that across the team's daily indexing waits and it adds up fast. After switching to Mutagen, that wait essentially disappeared.
Setting up a bidirectional sync session with Mutagen
Installation and initial connection
Mutagen ships as a single binary and doesn't require any persistent background service on either the local machine or the cloud Mac — it automatically deploys an agent to the remote side when the connection is established. Install it locally first:
brew install mutagen-io/mutagen/mutagen
mutagen version
Assuming SSH is already enabled on your cloud Mac (credentials are in the provisioning email), create a sync session:
mutagen sync create \
--name=ios-app \
--ignore-vcs \
--symlink-mode=posix-raw \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
--ignore-vcs automatically skips internal .git objects (git's own protocol is a better fit for syncing itself — no need to have Mutagen duplicate that work), while --symlink-mode=posix-raw preserves the symbolic links commonly found in CocoaPods without converting them.
Ignore rules: directories that should never be synced
You should always create a .mutagenignore file at the project root — otherwise the very first sync will drag several gigabytes of cache directories along with it:
DerivedData/
Pods/
.build/
node_modules/
*.xcuserstate
xcuserdata/
.DS_Store
What these directories have in common is that they can be regenerated locally on either side. Syncing them wastes bandwidth and — as the next section explains — can also introduce real problems.
Common Xcode pitfalls
- Never sync DerivedData: The module cache (ModuleCache.noindex) and build indexes inside it are tightly bound to the local machine's absolute paths and architecture. Syncing it to another machine essentially forces a full reindex, and in worse cases produces baffling compile errors. Let each side generate its own independently — it's meant to be disposable anyway.
- Install Pods and node_modules separately on each side: The files that actually need syncing are
Podfile.lockandpackage-lock.json. Runpod installandnpm installindependently on each side — this saves bandwidth and avoids cross-architecture incompatibilities with binary artifacts. - Keep
.gitignoreand.mutagenignorein sync: If these two ignore lists diverge, git will report "no changes" while Mutagen quietly transfers a pile of irrelevant files in the background — a frustrating thing to debug. It's worth writing a small script to diff the two lists after every change. - Watch out for permission bit differences: The cloud Mac and your local account often have different UIDs. If you hit permission errors, check whether sync stripped the executable bit —
--permissions-mode=portableresolves most of these cases.
Real-world performance and conflict handling
In day-to-day use, an incremental sync triggered by a save (a handful of changed Swift files) typically completes within one or two seconds — indistinguishable from a local save. The first full sync (tens of thousands of source files) is noticeably slower since it has to scan the entire directory tree; it's worth running it once before you start working rather than waiting on it mid-edit.
For conflict handling, the default bidirectional mode is two-way-safe — it won't auto-overwrite on conflict, instead pausing and waiting for you to resolve it manually. That's safer than a force-overwrite approach, but if both sides are editing the same file, you'll hit frequent pauses. For team collaboration, we recommend:
mutagen sync create \
--name=ios-app \
--sync-mode=two-way-resolved \
--default-file-mode-alpha=0644 \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
two-way-resolved lets the local side (alpha) win by default on conflicts. Combined with a team convention of "only one person edits a given file at a time on one side," this rarely loses changes. If you do hit a conflict on binary assets (images, fonts), mutagen sync list will show the conflicting paths, and you can compare timestamps and file sizes manually to decide which version to keep.
A ready-to-use config
Mutagen supports project-level config files. Place one at the project root as mutagen.yml, and team members can spin up sync with a single command after cloning the repo — no need to type out all the flags by hand:
sync:
ios-app:
alpha: "."
beta: "ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app"
mode: "two-way-resolved"
ignore:
vcs: true
paths:
- "DerivedData"
- "Pods"
- "node_modules"
- "xcuserdata"
- ".build"
symlink:
mode: "posix-raw"
A handful of commands cover most day-to-day needs:
mutagen sync list
mutagen sync monitor ios-app
mutagen sync pause ios-app
mutagen sync resume ios-app
mutagen sync terminate ios-app
monitor is especially useful when you're trying to figure out why a change hasn't propagated — it shows in real time which stage (scanning, staging, or transferring) is currently the bottleneck.
Pre-launch checklist
- Do both
.mutagenignoreand.gitignorecover rebuildable directories like DerivedData, Pods, node_modules, and xcuserdata? - Which sync mode are you using —
two-way-safeortwo-way-resolved? For team collaboration, prefer the latter and agree on whose changes take priority. - Was the initial full sync run during a quiet period with no urgent deadlines? An initial scan of tens of thousands of files isn't something to wait on while actively coding.
- Have you confirmed the cloud Mac's specific model and node configuration in the console? Bandwidth and disk specs directly cap your sync speed.
- Has
mutagen sync monitorbecome a habit for troubleshooting, rather than something you only check after files go missing?
Work through this list and your cloud Mac editing experience should feel close to native — and your team won't be pointing fingers over whose changes didn't make it across.
Frequently asked questions
Why not just mount the cloud Mac over SMB or NFS?
Network mounts turn every small file access (Xcode indexing, Pods lookups) into a round trip; large iOS projects can take tens of seconds just to open. Mutagen keeps a local cache and syncs incrementally, so editor responsiveness stays close to local speed and a dropped connection doesn't freeze your filesystem.
Should DerivedData be synced too?
No. DerivedData's module cache and indexes are tied to the machine's architecture and absolute paths; syncing it usually triggers a full re-index or build errors. Add it to .mutagenignore and let each machine regenerate its own.
What happens when a two-way sync conflicts?
Run mutagen sync list to find the conflicting paths. For most source-code conflicts, two-way-resolved mode with the local (alpha) side winning is enough; for binary assets, diff manually before deciding. Running git status before starting a sync session avoids most conflicts in the first place.
HireVPS
Try a dedicated cloud Mac mini today
Rent by the day, get SSH/VNC credentials in 2 minutes, and upgrade your configuration anytime.