MutagenでDockerのファイル同期速度を最適化する — WindowsとmacOS対応ガイド

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

Bind mountが遅い — macOS・WindowsでDockerを使う開発者の悩み

macOSまたはWindowsでDocker Desktopを使っているなら、こんな経験があるはずです。Linuxサーバーでは快適に動くアプリが、ローカル環境では不快なほど遅い。アセットのリビルドに数分かかり、ホットリロードに数秒かかる。コンテナ内でのnpm installはホストで直接実行する場合の5〜10倍もかかることも。

根本的な原因はbind mountの仕組みにあります。ホストのディレクトリをコンテナにマウントする際、DockerはmacOS/WindowsのファイルシステムとLinuxカーネルの間にある変換レイヤーを経由しなければなりません。macOSではosxfs(旧式)またはVirtioFS(新しい方式)がそのレイヤーに相当します。Windowsの場合はWSL2のファイルシステムブリッジです。ファイルが変更されるたびに、このレイヤーを通じて同期が行われ、そこでI/O速度が「消えていく」のです。

私はあらゆる方法を試しました。:cached:delegatedフラグの調整、VirtioFSの有効化、ソースコードをWSL2に移す方法など。しかし最終的に最も効果的だったのがMutagenでした。

Mutagenとは何か、なぜ速いのか

Mutagenは高性能なファイル同期ツールで、当初は独立したプロジェクトとして開発され、後にDocker DesktopにMutagenベースのファイル共有として統合されました。基本的な仕組みとして、直接bind mountする代わりに、Mutagenはコンテナ内の内部ボリュームにファイルのコピーを作成し、delta-sync方式(rsyncと同様に差分のみを転送)で双方向同期を行います。

その結果、コンテナはブリッジレイヤーを一切経由せず、純粋なLinuxボリュームからファイルを直接読み取ります。Mutagenはバックグラウンドでホストとコンテナ間の変更を静かに同期し続けます。ファイルの読み書き速度は大幅に向上し、通常のbind mountと比べて10〜20倍速くなることが一般的です。

Mutagenには2つの使い方があります:

  • Mutagen standalone:単独でインストールし、mutagen.ymlファイルで同期セッションを管理する
  • Mutagen Composedocker-composeの代替として使用し、compose.yml内のx-mutagen拡張を自動的に読み込む

2番目の方法の方がはるかにシームレスに統合できるため、私は日常的にこちらを使っています。

実際のインストールと設定

ステップ1:Mutagenをインストールする

macOSの場合、Homebrewを使います:

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

Windowsの場合、MutagenのGitHub releasesからバイナリをダウンロードしてPATHに追加するか、Scoopを使います:

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

インストールの確認:

mutagen version
mutagen-compose version

ステップ2:compose.ymlを設定する

次のような構成のNode.jsプロジェクトがあるとします:

my-app/
├── compose.yml
├── package.json
├── src/
└── node_modules/   ← これは同期すべきでない

通常のcompose.ymlではbind mountが使われています:

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

Mutagenを使う場合はこう書き換えます:

services:
  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - app_sync:/app          # 内部ボリューム — 速い!
    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"

ignore.pathsの設定が重要です:node_modulesは絶対に同期しないでください。このディレクトリはdocker-compose run app npm installでコンテナ内に直接インストールすべきであり、ホストからコピーするものではありません。これはMutagenを初めて使う人が最もよくやる間違いです。

ステップ3:mutagen-composeでプロジェクトを起動する

# docker composeの代わりにmutagen-composeを使う
mutagen-compose up -d

# 同期状態を確認する
mutagen sync list

# リアルタイムで監視する
mutagen sync monitor

初回起動時、Mutagenはホストからボリュームへすべてのファイルをコピーします(initial syncと呼ばれます)。この処理はプロジェクトの容量によって数秒から数分かかります。その後は変更されたファイルのみが同期され、ほぼ瞬時に反映されます。

ステップ4:長いコマンドを覚えなくて済むようにエイリアスを設定する

~/.zshrcまたは~/.bashrcに追加します:

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

実践から得たヒント集

同期モード:ユースケースに合った選択をする

Mutagenには3つの主要なモードがあります:

  • one-way-replica:ホスト → コンテナへの一方向同期で、コンテナからの書き戻しはできません。読み取り専用のアセット/メディアに適しています。
  • two-way-safe:双方向同期で、競合が発生すると処理を停止します。安全ですが、手動での対応が必要な場合があります。
  • two-way-resolved:双方向同期で、競合時はホスト側が常に優先されます。コードエディタはホスト上、出力はコンテナからという開発ワークフローに最適です。

ほとんどのWebプロジェクトでは、two-way-resolvedが最も合理的な選択です。

同期が「詰まった」ときの対処法

ときにMutagenが競合やセッションのフリーズを報告することがあります。素早くリセットするためのコマンドを紹介します:

# 詳細な状態を確認する
mutagen sync list --long

# 競合を手動でリセットする
mutagen sync reset app

# さらに悪い場合は、フラッシュして最初から同期し直す
mutagen sync flush app

# すべてのセッションを終了する(mutagen-compose downが自動で行う)
mutagen sync terminate --all

.dockerignoreと組み合わせて使う

.dockerignoreファイルはMutagenに影響しませんが、x-mutagen内のignoreリストは影響します。混乱を避けるため、私は両方のignoreリストを同一に保つようにしています:

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

# compose.yml の x-mutagen ignore(同じ内容)
ignore:
  paths:
    - ".git"
    - "node_modules"
    - ".next"
    - "dist"
    - "*.log"

コンテナ内でのAPIレスポンスのデバッグ

コンテナ内のサービスをデバッグしてJSONレスポンスを読む必要があるとき、私はよくdocker execでcurlして出力をコピーします。長いJSONをターミナルに貼り付けて読むのはかなり辛いので、toolcraft.appのjson-formatterに貼り付けて整形することが多いです。拡張機能のインストールよりも速く、VS Codeのタブを追加で開く必要もありません。

MutagenとDocker Desktop VirtioFS

VirtioFSが有効になった新しいバージョンのDocker Desktopを使っている場合、bind mountの速度は旧来のosxfsと比べて大幅に改善されています。それでもMutagenはほとんどのベンチマークで上回ります。特にPHP/WordPressのように小さなファイルが多いプロジェクトや、node_modulesが大きなプロジェクトでは顕著です。ただし、小さくシンプルなプロジェクトであれば、VirtioFS + 通常のbind mountで十分な場合もあります。

簡単な確認方法:bind mountと通常のボリュームでコンテナ内のtime npm installを実行して比較します。差が2倍未満であればMutagenは不要です。5倍以上の差があれば、今すぐMutagenを導入する価値があります。

まとめ

Mutagenは万能薬ではありません。ワークフローに複雑さのレイヤーを一つ加えることになり、セッションがフリーズした際にはデバッグが必要なこともあります。しかし、コードベースが大きいプロジェクトや、WindowsあるいはmacOSをメインの開発機として使うチームにとっては、最初から導入する価値のあるツールです。遅さにイライラしてから検討するのではなく、最初から取り入れることをお勧めします。

私が最も価値を感じているのはホットリロードの速さです。コードの変更がコンテナにほぼ瞬時に反映されると、フィードバックループが明らかに短くなります。そしてそれは日々の作業効率に直接影響するものです。

Share: