ローカルとコンテナ内のボリュームを同期できる Mutagen というものを最近知ったので使ってみました。
類似のツールに docker-sync がありますが、docker-sync は ruby 製なのと、同期のために別途 Unison が必要なのに対して、Mutagen は Go 言語製で依存が少なく、インストールや設定などが楽です。
インストール
Homebrew(Linuxbrew) でインストールできます。
brew install mutagen-io/mutagen/mutagen
ただし Docker Compose と連携する mutagen compose
コマンドを使うためには beta を入れる必要があります。
brew install mutagen-io/mutagen/mutagen-beta
ローカルのディレクトリの同期
次のコマンドでローカルのディレクトリ同士で同期できます。
mutagen sync create aaa/ bbb/
このコマンドは直ちに応答を返し、バックグラウンドで Mutagen デーモンなるものが起動し、そのデーモンが実際の同期の処理を実行します。
下記のコマンドで同期を止められます。
mutagen sync terminate --all
--all
なので実行中のすべての同期が終了します。mutagen sync create
の --name
オプションであらかじめ名前を付けておけば名前を指定して終了することもできます(他にもラベルを付けておいてラベルを条件にしたり、自動生成される ID を指定したりもできます)。
また、これだけだとデーモンは起動しっぱなしです。下記のコマンドでデーモンも止められます。
mutagen daemon stop
ローカルと Docker コンテナの同期
↑の例だとローカル同士の同期であまり嬉しくありません。次のようにローカルのディレクトリの代わりに Docker のコンテナ名とそのコンテナ内のディレクトリを指定すれば、ローカルと Docker コンテナの同期ができます。
mutagen sync create ./ docker://container_name/code
Docker コンテナ側に事前の仕込みは必要ありません。このコマンドで同期を開始するときに docker cp
で Mutagen エージェントなるものがコンテナにインストールされ、docker exec
で Mutagen エージェントを実行し、標準入出力を介してコンテナ側の Mutagen エージェントとホスト側の Mutagen デーモンが通信して同期が処理されます。
ローカルと リモート SSH サーバの同期
Docker だけではなくリモートの SSH サーバのディレクトリとも同期できます。次のように scp っぽく名前を指定するだけです。
mutagen sync create ./ ore@ore-no-server:/path/to/code
SSH 経由なら Unison で良いのでは・・という気もしますが、リモート側は SSH さえ繋がれば事前の準備が不要なので Unison よりもお手軽です。
Unison はローカルとリモートでバージョンがちょっと違うだけで動かなかったりするし・・Unison のバージョンが同じでもビルドした OCaml コンパイラのバージョンが違うとダメだったりするし、バージョンに対してセンシティブすぎる感があります。
なお、これまでの例では同期の片側をローカルにしていましたが、リモートからリモートも可能です。例えば SSH サーバと Docker コンテナの同期なども可能です。
mutagen sync create ore@ore-no-server:/path/to/code docker://container_name/code
ネットワーク転送
Mutagen は Unison のようなファイル同期だけでなく、SSH の -L
や -R
のようなポートフォワーディングも可能です。
下記のコマンドでローカルの 11111 ポートでリッスンし、ローカルの 22222 ポートへ転送されます。ローカル同士で転送してもあまり意味は無いですけど。
mutagen forward create tcp:localhost:11111 tcp:localhost:22222
このコマンドも直ちに応答を返し、実際の転送はバックグラウンドで実行される Mutagen デーモンによって処理されます。下記のコマンドで転送を終了できます。
mutagen forward terminate --all
Docker コンテナ内のポートへも転送できます。
# ローカルからコンテナへ mutagen forward create tcp:localhost:11111 docker://container_name:tcp:localhost:11111 # コンテナからローカルへ(いわゆる逆フォワーディング) mutagen forward create docker://container_name:tcp:localhost:22222 tcp:localhost:22222
もっともこんなことをしなくても「ローカルからコンテナ」はコンテナのポートを expose すれば良いだけだし、「コンテナからローカル」はコンテナから host.docker.internal
に接続すれば良いだけなので、わざわざ Mutagen を使う必要は無さそうな気もしますが・・
Docker Desktop なら確かにそうなのですが、Docker Remote API でリモートの Docker ホストを開発に使用しているときはコンテナのポートを expose してもリモートの Docker ホスト上のポートで公開されるだけだし、逆方向はリモートの Docker ホストが NAT の外側にあるとコンテナの中からローカルのポートにアクセスするのは非常に困難です。
Mutagen であればローカルからリモートホストに Docker でさえ繋がればあとは docker cp
なり docker exec
なりで同期も転送も出来るので、ローカルと Docker ホストが NAT で隔てられていたとしても問題ありません。
mutagen compose
これまでの例では同期や転送を個別にコマンドで開始していますが、実際のところそのような使い方はせず、mutagen compose
または後述の mutagen project
で複数の同期や転送をまとめて管理することになります。
mutagen compose
コマンドでは Mutagen を Docker Compose に統合できます。
まず docker-compose.yml
、または docker-compose.override.yml
などに、x-mutagen
という名前で Mutagen の設定を記述します。x-
から始まるので docker-compose
コマンドからはこれは無視されます。
version: '3.7' networks: frontend: volumes: code: services: app: # ...snip... networks: - frontend volumes: - code:/code:rw x-mutagen: sync: defaults: mode: two-way-resolved stageMode: neighboring permissions: defaultFileMode: 0644 defaultDirectoryMode: 0755 ignore: vcs: true paths: - /vendor/ app: alpha: . beta: volume://code forward: app: source: tcp:localhost:9876 destination: network://frontend:tcp:app:9876
次のコマンドで同期と転送を開始します。
mutagen compose up
mutagen compose
は docker-compose
のラッパーです。元の Docker Compose の構成に mutagen
サービスを追加するための YAML が作成され、docker-compose
の -f
オプションに指定して mutagen
サービスを開始し、サービスが開始して同期や転送の準備が出来たところで docker-compose up
が実行されます。
追加で作成される YAML の mutagen
サービスには、docker-compose.yml
の x-mutagen
に記載された volume://
や network://
に基づいて必要なボリュームやネットワークがアタッチされます。
例えば↑の docker-compose.yml
からは /tmp/io.mutagen.compose.999999999/mutagen.yml
のようなファイル名で次のファイルが作成されます。
version: "3.7" services: mutagen: image: mutagenio/sidecar:latest networks: - frontend volumes: - code:/volumes/code
そしてこのコンテナに Mutagen エージェントがインストールされ、docker exec
でローカル側の Mutagen デーモンとコンテナ内の Mutagen エージェントが通信して同期や転送が実行されます。
mutagen compose で Reverse forwarding の代替
mutagen compose
の制限について Known limitations | Compose | Mutagen に記載があります。特に辛いのが Reverse forwarding(逆フォワーディング)ができないことです。リモートの Docker ホストを開発に使う場合、逆フォワーディングができないと「コンテナ→ローカル」の通信を通すことができません。
ので、SSH のポートフォワーディングを使った代替を考えてみました。
まず、docker-compose.yml
に ngyuki/insecure-sshd イメージのサービスを追加します。ngyuki/insecure-sshd
はパスフレーズなしで root ログインできる sshd を実行する Docker イメージです。このサービスには host.docker.internal
でアクセスできるようにネットワークのエイリアスも指定しておきます。
そして Mutagen の forward でローカルの適当なポートから ngyuki/insecure-sshd
の 22 ポートへの転送を指定します。
version: "3.7" services: app: # ...snip... fwd: image: ngyuki/insecure-sshd networks: frontend: aliases: - host.docker.internal x-mutagen: forward: fwd: source: tcp:localhost:2222 destination: network://frontend:tcp:fwd:22
mutagen compose up
で開始した後、ローカルから次のように SSH を実行します。
ssh root@localhost -p 2222 -C -N -g \ -o ExitOnForwardFailure=yes \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -R 5000:localhost:5000
これで app のコンテナから host.docker.internal:5000
に接続すればローカルの 5000 ポートに転送されます。
なお、Mutagen のネットワーク転送を使わなくても、次のように ProxyCommand
で docker-compose exec
を使っても良いです。この方法なら 「ローカルの適当なポート」 を考える必要が無いのが良いですね。
ssh root@localhost -C -N -g \ -o ProxyCommand='docker-compose exec -T fwd nc localhost 22' \ -o ExitOnForwardFailure=yes \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -R 5000:localhost:5000
mutagen project
mutagen project
コマンドは複数の同期や転送の設定を一つのファイルに記述して、コマンド一発でそれらを開始したり停止したりできます。前述の mutagen compose
は逆フォワーディングをサポートしていないため、代替として mutagen project
を使ってみます。
まず、docker-compose.yml
に次のように mutagen
サービスを追記します。
version: '3.7' networks: frontend: volumes: code: services: app: # ...snip... networks: - frontend volumes: - code:/code:rw mutagen: container_name: ore-no-mutagen image: alpine networks: frontend: aliases: - host.docker.internal volumes: - code:/code:rw command: - tail - -f - /dev/null
要するに mutagen compose
で自動生成されていたサービスを自前で用意しておく感じです。自動生成されるものはイメージが mutagenio/sidecar:latest
でしたが、これは単に SIGTERM
を受けるまで待ち続ける alpine ベースのイメージなので、上記のように tail -f /dev/null
でも問題ありません。
また、後述の mutagen.yml
でコンテナ名をベタ書きするために mutagen
サービスは container_name
でコンテナ名も指定しておきます。未指定ならコンテナ名は「ディレクトリ名サービス名連番」のような規則で生成されるので、mutagen.yml
でその名前で指定しても良いとは思います。
次に mutagen.yml
を下記のような内容で作成します。
sync: defaults: mode: two-way-resolved stageMode: neighboring permissions: defaultFileMode: 0644 defaultDirectoryMode: 0755 ignore: vcs: true paths: - /vendor/ code: alpha: . beta: docker://ore-no-mutagen/code/ forward: app: source: tcp:localhost:9876 destination: docker://app:tcp:localhost:9876 reverse: source: docker://ore-no-mutagen:tcp:0.0.0.0:5000 destination: tcp:localhost:5000
要するに mutagen compose
で x-mutagen
に記述していた内容ですが、volume://
や network://
は使えないため、コンテナ名をベタ書きする必要があります。
docker-compose
および mutagen project
を開始します。
docker-compose up -d mutagen project start
これで次のことが行われます。
- ローカルのカレントディレクトリが code ボリュームに同期
- ローカルの 9876 ポートを app サービスの 9876 ポートに転送
- mutagen サービスの 5000 ポートを ローカルの 5000 ポートに転送
mutagen compose
と比べると逆ポートフォワードも簡単にできて良さそうです。下記の公式のサンプルのように beforeCreate
や afterCreate
なども使えば mutagen project
だけで docker-compose
も一緒に実行できます。
さいごに
以前、リモートの Docker ホストを開発に使うために Unison と SSH ポートフォワードで次のように環境構築していました。
- Docker Desktop が重いのでリモートサーバの Docker で開発できるようにしてみた
- NAT の内側からリモートのサーバを使って PhpStorm+Xdebug+Docker で開発するメモ
これはこれでうまく機能していたのですが、いかんせん面倒すぎました。Mutagen なら特に難しいこともなくサクッと解決させられそうです。
ただ、Mutagen が原因なのかこっちの環境(WSL1 の DrvFs 上で実行)の問題なのかわかりませんが、たまに Mutagen デーモンが応答不能になって、kill しても死なないし Mutagen から実行されている docker exec
を停止させるとゾンビになり、WSL そのものをシャットダウンしようとしても応答不能で WSL を開始も停止もできなくなることがあり、Windows 自体を再起動するしかなくなることがあります。
Unison でもたまに unison-fsmonitor のプロセスが殺せなくなることがあったので、たぶん WSL1 の DrvFs 上で実行していることが原因な気がします。Unison は WSL ではなく Windows 側で unison.exe を実行するようにすれば解消されたのですが、Mutagen も Windows 側で実行するぐらいなら Unison で良いかな・・という気もしてます。
あるいは WSL2 の ext4 上で実行するようにしても良いと思うのですが・・なんとなくコードは Windows 側にチェックアウトしておきたいんですよね・・WSL2 の 9p だといろいろ制限があるため(ネイティブの監視が効かないなど)、それなら WSL1 の DrvFs の方がマシかな、と思って未だに WSL1 使ってます。
また、Mutagen はバックグラウンドで実行されるデーモンが同期や転送を処理するため、同期や転送を止め忘れてしまうことがちょいちょいあります。コンソールを専有しなくて済む、というメリットもあるのかもしれませんが、個人的にはフォアグラウンドで実行される方が好みです(同じ理由で dokcer-compose up
も -d
は付けない派)。