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 run sends the entire workspace.
  • Worktree switches. coast assign sends only the delta between the old and new worktree. Files that did not change are not retransmitted.
  • Compression. The -z flag 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 build from [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.