Skip to content
COMA

Source sync

COMA keeps a copy of your project on the machine so bind mounts see your code, without deleting or overwriting what changed there.

COMA keeps your workspace's files on the machine so bind mounts see your code. Files in .gitignore, .comaignore and .git/ are not copied.

Why sync exists

A Compose file with ./src:/app/src asks the engine to mount a directory. The engine runs on the machine, so with plain Docker over SSH it mounts whatever is at that path on the machine, usually nothing.

COMA copies the directory to the machine and keeps the copy current. When a container is created through COMA's endpoint, COMA rewrites each bind source inside a synced directory to the machine's copy (~/.coma/workspaces/<id>/source). Your Compose file is not changed.

Before it creates the container, COMA waits for sync to catch up, for at most 30 seconds. A container created right after you save a file sees the edit. If sync cannot catch up in time, the container is not created and the client gets sync_barrier_timeout.

Starting sync

  • coma workspace up syncs a workspace's source.
  • coma sync watch syncs a directory without a coma.yaml: the current directory, or --path, to the current context's machine or --machine.

Sync runs in comad, COMA's background process, so it continues after the command returns.

CommandWhat it does
coma sync statusShow synced directories, their mode and state, conflicts and problems
coma sync explain <path>Say whether a path is synced and which rule decided
coma sync resolve <path>Settle a conflict; takes --keep local or --keep machine
coma sync stopStop syncing a directory; its copy stays on the machine

One machine cannot sync two directories nested inside each other.

What is synced

Everything in the directory is synced except:

  • .git/ and .coma/, always;
  • paths matched by .comaignore at the root of the directory;
  • paths matched by .gitignore files, including nested ones;
  • in a workspace, spec.sync.ignore patterns. spec.sync.include re-includes paths, and spec.sync.gitignore: false stops using .gitignore files.

Patterns use Git syntax. A rule change applies within about five seconds. To see which rule decided a path, ask:

coma sync explain node_modules
node_modules: not synced (.gitignore: node_modules/)

Other paths answer the same way:

src/app.js: synced (no rule matches; synced)
.git/config: not synced (inside .git/, excluded by built-in: .git/)

Sync copies .env files you have not ignored. coma workspace plan warns about them.

How fast

Changes usually reach the machine in about a second. On macOS, COMA watches files with FSEvents and uses almost no CPU while idle. On Linux, recently changed files are watched and the rest are checked every two seconds.

Distance matters: on a machine about 300 ms away, edits reached the machine in about 1.5 seconds. Choose a nearby region when you can.

The default mode never loses work on the machine

The default mode is one-way-safe. Your computer is the source; the machine's copy follows it.

  • A file you create or change locally is copied to the machine.
  • A file you delete locally is deleted on the machine only if it is unchanged there.
  • A file created on the machine, for example by a container, is kept.
  • A file changed on the machine is never overwritten. If you also change it locally, that is a conflict: the machine's copy stays, and coma sync status lists it.

Settle a conflict by choosing which copy wins:

coma sync resolve src/app.py --keep local

--keep local copies your version to the machine. --keep machine copies the machine's version to your computer. The conflict clears once sync has caught up. Only files can be resolved, not directories.

Opt-in modes

Two other modes exist. Both need explicit opt-in, and both can remove or overwrite files the default mode would keep.

ModeWhat it doesHow to turn it on
one-way-mirrorThe machine's copy mirrors yours: files there that you do not have are deletedspec.sync.mode: one-way-mirror in coma.yaml, then confirm on the first workspace up
bidirectionalChanges on either side propagate in both directionsspec.sync.mode: bidirectional in coma.yaml, and your own COMA config

one-way-mirror. Before the first sync in this mode, workspace up asks for confirmation (--yes in scripts; without a terminal it fails with confirmation_required). Deletions on the machine cannot be undone from COMA. Ignored paths are never touched, so data you keep in ignored directories survives.

bidirectional. A repository's coma.yaml cannot turn this on by itself, because files written by that repository's containers would then flow back into your checkout. You enable it in your own config file (coma doctor prints its path):

sync:
  allowBidirectional: true

or for one shell with COMA_SYNC_ALLOW_BIDIRECTIONAL=1. Without it, workspace plan and workspace up are blocked and name the setting. In this mode a deletion propagates only if the other side did not change the file, and a file changed on both sides is a conflict you settle with coma sync resolve.

spec.sync.mode: none turns sync off for a workspace.

Bind mounts COMA refuses

A bind source outside every synced directory is refused before it reaches the engine, with sync_bind_source_unmanaged. Without this, the engine would mount an empty directory it created on the machine. A source that is ignored or could not be synced is refused with sync_bind_source_missing, naming the rule. Run coma sync explain on the path to see why.

Engine sockets are the exception: a bind of /var/run/docker.sock or COMA's local socket is mapped to the engine's socket on the machine, which tools such as Testcontainers need.

Machine-local data directories

Compose files often mount data directories that are gitignored and absent from your computer, such as ./data:/var/lib/postgresql/data. Docker would create such a directory when it is missing. COMA lets it: a missing source, or an empty ignored directory, is mapped into the machine's copy, where the engine creates it. The data stays on the machine and is not synced back.

An ignored file, or an ignored directory that has content locally, is still refused, with a hint to add it to spec.sync.include. Starting it empty on the machine would silently lose that content.

Podman's own API never creates a missing bind source, so this applies to Docker-style requests only.

The sync helper

Sync runs on Mutagen, shipped with COMA as a separate helper, coma-sync. It is built from Mutagen's MIT-licensed code only, without the parts under other licences. The helper connects through COMA with COMA's strict host keys and SSH keys, never your ~/.ssh configuration. coma doctor reports which sync engine is in use.

Without the helper, COMA falls back to a simpler built-in engine (also selected with COMA_SYNC_ENGINE=builtin). It never deletes files on the machine, but it does not detect conflicts: a file changed on the machine is overwritten the next time you change it locally. It does not run the opt-in modes.

Next

On this page