File Sync
Remote coasts use a two-layer sync strategy: rsync for bulk transfers, mutagen for continuous real-time sync. Both tools are runtime dependencies installed inside coast containers -- they are not required on your host machine.
Where Sync Runs
Local Machine Remote Machine
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ coastd daemon │ │ │
│ │ │ │ │
│ │ rsync (direct SSH) │ SSH │ /data/workspaces/{p}/{i}/ │
│ │────────────────────────│───────▶│ (rsync writes here) │
│ │ │ │ │ │
│ │ docker exec │ │ │ bind mount │
│ ▼ │ │ ▼ │
│ Shell Container │ SSH │ Remote DinD Container │
│ /workspace (bind mount) │───────▶│ /workspace │
│ mutagen (continuous sync)│ │ (compose services running)│
│ SSH key (copied in) │ │ │
└─────────────────────────────┘ └──────────────────────────────┘
The daemon runs rsync directly from the host process. Mutagen runs inside the local shell container via docker exec.
Layer 1: rsync (Bulk Transfer)
On coast run and coast assign, the daemon runs rsync from the host to transfer workspace files to the remote:
rsync -rlDzP --delete-after \
--rsync-path="sudo rsync" \
--exclude '.git' --exclude 'node_modules' \
--exclude 'target' --exclude '__pycache__' \
--exclude '.react-router' --exclude '.next' \
-e "ssh -p {port} -i {key}" \
{local_workspace}/ {user}@{host}:{remote_workspace}/
After rsync completes, the daemon runs sudo chown -R on the remote to give the SSH user ownership of the files. rsync runs as root via --rsync-path="sudo rsync" because the remote workspace may contain root-owned files from coast-service operations inside the container.
What rsync does well
- Initial transfers. The first
coast runsends the entire workspace. - Worktree switches.
coast assignsends only the delta between the old and new worktree. Files that did not change are not retransmitted. - Compression. The
-zflag compresses data in transit.
Excluded paths
rsync skips paths that should not be transferred:
| Path | Why |
|---|---|
.git |
Large, not needed on remote (worktree content is sufficient) |
node_modules |
Rebuilt inside DinD from lockfiles |
target |
Rust/Go build artifacts, rebuilt on remote |
__pycache__ |
Python bytecode cache, regenerated |
.react-router |
Generated types, recreated by dev server |
.next |
Next.js build cache, regenerated |
Protecting generated files
When coast assign runs with --delete-after, rsync normally deletes files on the remote that do not exist locally. This would destroy generated files (like proto clients at generated/) that the remote dev server created but your local worktree does not contain.
To prevent this, rsync uses --filter 'P generated/***' rules that protect specific generated directories from deletion. The protected paths include generated/, .react-router/, internal/generated/, and app/generated/.
Partial transfer handling
rsync exit code 23 (partial transfer) is treated as a non-fatal warning. This handles a race condition where running dev servers inside the remote DinD regenerate files (e.g., .react-router/types/) while rsync is writing. Source files transfer successfully; only generated artifacts may fail, and those are regenerated by the dev server anyway.
Layer 2: mutagen (Continuous Sync)
After the initial rsync, the daemon starts a mutagen session inside the local shell container:
docker exec {shell_container} mutagen sync create \
--name coast-{project}-{instance} \
--sync-mode one-way-safe \
--ignore-vcs \
--ignore node_modules --ignore target \
--ignore __pycache__ --ignore .next \
/workspace/ {user}@{host}:{remote_workspace}/
Mutagen watches for file changes via OS-level events (inotify inside the container), batches changes, and transfers deltas over a persistent SSH connection. Your edits appear on the remote within seconds.
One-way-safe mode
Mutagen runs in one-way-safe mode: changes flow from local to remote only. Files created on the remote (by dev servers, build tools, etc.) are not synced back to your local machine. This prevents generated artifacts from polluting your working directory.
Mutagen is a runtime dependency
Mutagen is installed in:
- The coast image (built by
coast buildfrom[coast.setup]), used by the local shell container. - The coast-service Docker image (
Dockerfile.coast-service), used on the remote side.
The daemon never runs mutagen directly on the host. It orchestrates via docker exec into the shell container.
Lifecycle
| Command | rsync | mutagen |
|---|---|---|
coast run |
Initial full transfer | Session created after rsync |
coast assign |
Delta transfer of new worktree | Old session terminated, new session created |
coast stop |
-- | Session terminated |
coast rm |
-- | Session terminated |
Fallback behavior
If the mutagen session fails to start inside the shell container, the daemon logs a warning. The initial rsync still provides the workspace content, but file changes will not sync in real time until the session is re-established (e.g., on the next coast assign or daemon restart).
Sync Strategy Configuration
The [remote] section of your Coastfile controls the sync strategy:
[remote]
workspace_sync = "mutagen" # "rsync" (default) or "mutagen"
rsync(default): only the initial rsync transfer runs. No continuous sync. Good for CI environments or batch jobs where real-time sync is not needed.mutagen: rsync for the initial transfer, then mutagen for continuous sync. Use this for interactive development where you want edits to appear on the remote immediately.