How to Use Mutagen to Speed Up Docker File Sync on Windows and macOS

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Slow bind mounts — the pain every Docker dev on macOS and Windows knows

If you’re running Docker Desktop on macOS or Windows, you’ve almost certainly hit this: the app runs great on a Linux server, but on your local machine it’s painfully slow. Asset rebuilds take a full minute, hot reload lags by several seconds, and npm install inside the container takes 5–10× longer than running it directly on the host.

The root cause is the bind mount mechanism — when you mount a directory from the host into the container, Docker has to pass through a translation layer between the macOS/Windows filesystem and the Linux kernel. On macOS, this is the osxfs layer (legacy) or VirtioFS (newer). On Windows it’s the WSL2 filesystem bridge. Every file change has to be synced through this intermediary — and that’s where I/O speed goes to die.

I tried everything: tuning the :cached and :delegated flags, enabling VirtioFS, moving source code into WSL2. But the solution that actually worked was Mutagen.

What is Mutagen and why is it faster?

Mutagen is a high-performance file synchronization tool, originally developed as a standalone project before being integrated into Docker Desktop as Mutagen-based file sharing. Instead of a direct bind mount, Mutagen creates a copy of your files inside a native container volume and syncs changes bidirectionally using delta-sync — only transferring what changed, similar to rsync.

The result: the container reads files from a pure Linux volume with no bridge layer in the way, while Mutagen quietly syncs changes from the host into the container and back. Read/write performance improves dramatically — typically 10–20× compared to a standard bind mount.

Mutagen supports two usage modes:

  • Mutagen standalone: installed separately, uses a mutagen.yml file to manage sync sessions
  • Mutagen Compose: a drop-in replacement for docker-compose that automatically reads the x-mutagen extension in your compose.yml

The second approach integrates far more cleanly, and it’s what I use every day.

Installation and real-world configuration

Step 1: Install Mutagen

On macOS, use Homebrew:

brew install mutagen-io/mutagen/mutagen mutagen-io/mutagen/mutagen-compose

On Windows, download the binary from the Mutagen GitHub releases page and add it to your PATH, or use Scoop:

scoop bucket add mutagen https://github.com/mutagen-io/scoop-bucket.git
scoop install mutagen mutagen-compose

Verify the installation:

mutagen version
mutagen-compose version

Step 2: Configure compose.yml

Say you have a Node.js project with this structure:

my-app/
├── compose.yml
├── package.json
├── src/
└── node_modules/   ← do NOT sync this

A typical compose.yml with a bind mount looks like this:

services:
  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - .:/app          # Bind mount — slow!
    command: npm run dev

Here’s the Mutagen equivalent:

services:
  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - app_sync:/app          # Native volume — fast!
    command: npm run dev

volumes:
  app_sync:

x-mutagen:
  sync:
    default:
      configurationBeta:
        permissions:
          defaultFileMode: "0644"
          defaultDirectoryMode: "0755"
  sync:
    app:
      alpha: "."
      beta: "volume://app_sync"
      mode: "two-way-resolved"
      ignore:
        paths:
          - ".git"
          - "node_modules"
          - ".next"
          - "dist"
          - "*.log"

The critical part is the ignore.paths section: never sync node_modules. That directory should be installed inside the container via docker-compose run app npm install, not copied from the host. This is the single most common mistake I see from developers new to Mutagen.

Step 3: Run your project with mutagen-compose

# Replace docker compose with mutagen-compose
mutagen-compose up -d

# Check sync status
mutagen sync list

# Monitor in real time
mutagen sync monitor

On the first run, Mutagen performs an initial sync — copying all files from the host into the volume. This takes anywhere from a few seconds to a few minutes depending on project size. After that, only changed files are synced, almost instantly.

Step 4: Add aliases so you don’t have to type the full commands

Add these to your ~/.zshrc or ~/.bashrc:

alias dc="mutagen-compose"
alias dcu="mutagen-compose up -d"
alias dcd="mutagen-compose down"
alias msl="mutagen sync list"

Tips from real-world experience

Sync modes: pick the right one for your use case

Mutagen has three main modes:

  • one-way-replica: host → container only; the container cannot write back. Use this for read-only assets or media.
  • two-way-safe: bidirectional sync that pauses on conflicts. Safe, but occasionally requires manual intervention.
  • two-way-resolved: bidirectional sync where the host always wins on conflicts. Best fit for a dev workflow — your editor lives on the host, output comes from the container.

For most web projects, two-way-resolved is the right default.

Handling a stuck sync session

Occasionally Mutagen reports a conflict or a sync session hangs. Quick commands to reset:

# Get detailed status
mutagen sync list --long

# Manually reset a conflict
mutagen sync reset app

# If things are worse, flush and re-sync from scratch
mutagen sync flush app

# Terminate all sessions (mutagen-compose down does this automatically)
mutagen sync terminate --all

Keeping .dockerignore in sync with Mutagen ignore

The .dockerignore file has no effect on Mutagen, but the ignore list in x-mutagen does. I keep both lists consistent to avoid confusion:

# .dockerignore
node_modules
.git
.next
dist
*.log

# compose.yml x-mutagen ignore (same list)
ignore:
  paths:
    - ".git"
    - "node_modules"
    - ".next"
    - "dist"
    - "*.log"

Debugging API responses inside the container

When debugging a service inside the container and needing to read JSON responses, I usually use docker exec to curl and copy the output. Pasting long JSON into the terminal to read is painful — I typically drop it into the json-formatter at toolcraft.app to get a clean, formatted view. It’s faster than installing an extension and doesn’t require opening another VS Code tab.

Mutagen vs Docker Desktop VirtioFS

If you’re on a recent version of Docker Desktop with VirtioFS enabled, bind mount performance has already improved significantly over the old osxfs layer. Mutagen still wins in most benchmarks, especially for projects with many small files like PHP/WordPress or those with large node_modules. For small, simple projects, VirtioFS with a standard bind mount may be plenty fast.

A quick sanity check: run time npm install inside the container with a bind mount versus a plain volume and compare. If the difference is under 2×, you probably don’t need Mutagen yet. If it’s 5× or more, it’s worth setting up right away.

Conclusion

Mutagen isn’t a silver bullet — it adds a layer of complexity to your workflow and occasionally needs attention when a session gets stuck. But for projects with large codebases, or teams using Windows or macOS as their primary dev machines, it’s worth setting up from day one rather than waiting until the slowness becomes unbearable.

The biggest win for me: hot reload that actually feels instant. When file changes land in the container almost immediately instead of after a multi-second delay, the feedback loop gets noticeably tighter — and that has a direct, daily impact on how productive you feel while coding.

Share: