TechEarl

Why Bind Mounts Are Slow on Mac and Windows (and What to Do)

Docker Desktop's virtualized filesystem makes bind mounts noticeably slow on Mac and Windows. VirtioFS helps a lot, the node_modules-as-named-volume trick helps more, and moving the project into WSL2 on Windows fixes it entirely.

Ishan KarunaratneIshan Karunaratne⏱️ 7 min readUpdated
Share thisCopied

Bind mounts on Linux are free — the container reads and writes the host's filesystem directly. On Mac and Windows, they go through a virtualized filesystem because Docker Desktop runs Linux in a VM, and your Mac or Windows filesystem isn't natively visible to that VM. The bridge between them is what slows things down.

The good news: it has gotten much better. The default Docker Desktop file-sharing mode (VirtioFS on Mac since 4.22, August 2023) is significantly faster than the gRPC FUSE it replaced. The remaining slowness mostly hits dependency directories like node_modules — the named-volume trick handles those.

Why it's slow

On Linux, when a container writes to /host/path:/path/in/container, the kernel just maps one path to another. Same filesystem, native speed.

On Mac and Windows, the Linux container is inside Docker Desktop's VM. Your project lives on macOS (APFS) or Windows (NTFS). For the container to see your files:

  1. macOS/Windows kernel reads the file.
  2. Docker Desktop's file-sharing layer (VirtioFS, gRPC FUSE, or the older osxfs) translates between the host filesystem and the Linux VM's filesystem.
  3. Linux kernel hands the bytes to the container.

Every file read or write crosses that translation boundary. For a single file, it's a millisecond of overhead. For node_modules with 100,000 small files, those milliseconds add up to seconds or minutes.

Fix 1: confirm you're on VirtioFS (Mac)

code
Docker Desktop → Settings → General → "Choose file sharing implementation for your containers"

Options (varies by version):

  • VirtioFS — default in Docker Desktop 4.22+ (August 2023). Significantly faster.
  • gRPC FUSE — older default, slower.
  • osxfs (legacy) — much older, very slow.

Pick VirtioFS if you're on a current Docker Desktop. If for some reason it's slower for your workload, fall back to gRPC FUSE.

On Windows, the WSL2 backend handles file sharing via 9P (the WSL filesystem protocol), and there isn't a comparable switch — the path matters more (see below).

Fix 2: named volume for node_modules

The classic trick. Your project source bind-mounts to /app, but node_modules mounts as a named volume:

yaml
services:
  app:
    image: node:22
    working_dir: /app
    volumes:
      - .:/app                              # bind-mount source for hot reload
      - app-node_modules:/app/node_modules  # named volume — Docker-managed
    command: npm run dev
    ports:
      - "3000:3000"

volumes:
  app-node_modules:

Why this works: the bind mount of .:/app overlays your host source onto the container's /app. The named volume of app-node_modules:/app/node_modules then overlays a Docker-managed location (which is in the VM's native filesystem, no translation needed) on top. So inside the container, node_modules is fast.

Trade-off: you can't see node_modules from your host's editor for things like "go to definition" jumping into a dependency. Most people don't care; if you do, install node_modules on the host too (the host's copy and the container's copy are now separate).

Same pattern for __pycache__, vendor/ (Composer, PHP), Bundler's vendor/bundle, Cargo's target/. Any large auto-generated dependency tree.

Fix 3: keep your project in WSL2 (Windows)

On Windows with WSL2-backed Docker Desktop:

  • Project in C:\Users\you\code\project (Windows filesystem): every bind-mount read crosses the 9P translation boundary. Slow.
  • Project in \\wsl$\Ubuntu\home\you\code\project (inside WSL2): the container reads the file natively from the WSL2 distro's filesystem. Fast.

The fix on Windows is to move the project into your WSL2 distro:

bash
# Open your WSL2 distro (e.g., Ubuntu)
cd ~
git clone https://github.com/.../my-project.git
cd my-project
docker compose up -d

Edit files with VS Code's Remote-WSL extension or another editor that supports WSL paths. Same Windows machine, dramatically faster Docker.

Fix 4: tmpfs for caches

For caches that don't need to survive a container restart, mount tmpfs (RAM-backed):

bash
docker run -d --tmpfs /app/cache:size=200m my-app

tmpfs is in-memory, so it's at native CPU speed regardless of platform. Useful for build caches, session stores, temp files.

Fix 5: avoid bind-mounting irrelevant directories

docker run -v $(pwd):/app bind-mounts everything in your project. If .git is 500 MB or you have a coverage directory or a dist from yesterday's build, they're all crossing the filesystem bridge whenever something walks the tree.

The fix: .dockerignore for build, and selective volume mounts for runtime. Instead of bind-mounting the whole project, mount only what the container actually needs:

yaml
volumes:
  - ./src:/app/src
  - ./public:/app/public
  - ./package.json:/app/package.json:ro

Trade-off: more verbose, but you mount only the live-edit directories.

Cached and delegated consistency (older flags)

yaml
volumes:
  - .:/app:cached       # host source is authoritative; container reads may lag
  - .:/app:delegated    # container's writes may lag the host

These flags came from the gRPC FUSE era and gave some speedup at the cost of consistency. With VirtioFS as the default, they're less relevant — and Docker Desktop ignores them on VirtioFS. If you find them in older Compose files, you can keep them (no harm) or remove them.

Common pitfalls

  • VirtioFS not on. Default since 4.22, but if you updated Docker Desktop from a much older version, the setting might have stayed on the old default. Check Settings → General.
  • Mounting the whole project including node_modules. The named-volume trick is exactly the workaround.
  • Editing on Windows side, container running in WSL2. That's the slow case. Move the project into WSL2.
  • Expecting Linux-level performance. Mac and Windows have a fundamental disadvantage here — Linux containers run in a VM. Even with VirtioFS, bind mounts are slower than native. Optimize what you can; for the absolute fastest dev loop, Linux remains best.

What to do next

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerMacWindowsPerformanceBind MountsVirtioFSDevOps

Found this useful? Pass it on.

Copied
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Rank the biggest files on a full disk with find -printf '%s %p' piped to sort -rn. The GNU one-liner, the BSD stat variant for macOS, why -xdev matters, human-readable sizes, and when du or ncdu beats find.

How to Find the Largest Files on Disk (find, sort, du)

find / -xdev -type f -printf '%s %p\n' | sort -rn | head -20 gives you a ranked list of the biggest files on a full disk. The GNU one-liner, the BSD/macOS stat variant, why -xdev matters, human-readable output with numfmt, when to switch to du or ncdu for per-directory totals, and the mistakes that send a scan into /proc.

Archive every file matching a find pattern with tar. The safe find -print0 | tar --null --files-from=- one-liner, the macOS BSD tar -T difference, archiving by modification time, and gzip vs bzip2 vs xz vs zstd.

How to Archive Files Matching a find Pattern with tar

find locates the files, tar archives them. The safe pairing is find -print0 piped into tar reading a NUL-delimited list from stdin: no breakage on spaces or newlines. The flag breakdown, the macOS BSD tar vs GNU tar difference, the -exec append alternative, archiving by modification time, and the compression choices.