docker-syncでホスト-コンテナ間を爆速で同期する

**2020/05/26 追記**

[Docker for Mac の Mutagen-based caching で Volume のパフォーマンスが劇的に改善した](https://qiita.com/nanasess/items/16ab9274c34bdc34e567)

Mutagen単独で試してみたことがあって、すごく速くてよかったんですが、 Docker for macに統合されそうな感じになってるんですね。
これは期待。

**2017/3/15 追記**

先日この問題のissueに対して、

https://github.com/docker/for-mac/issues/77#issuecomment-283996750

というコメントがつけられ、それに関する 

https://github.com/docker/docker/pull/31047

というプルリクが 本体にマージされたようです。

まだ詳しく見ていませんが、マウント時に同期方法オプションが指定できるようになり、そのオプションによってキャッシュするレベルを制御して同期を軽くしよう、というような感じになるようでした。

実際にリリース版で使えるのはいつなのかわかりませんが、やはり本体が早くなるのが一番いいのでちょっと期待ですね。

========== **追記ここまで** ==========

# docker-syncでホスト-コンテナ間を爆速で同期する

## 問題

少し前にprivate betaが終わって、`docker for mac` を使っている方も結構いらっしゃいますね。
で、僕も`docker for mac`は使っているのですが、ホスト側のディレクトリをマウントして使うと猛烈に遅い。

`docker-toolbox（vitual box）` を使っていた頃も同じ問題はあったのですが、[docker-machine-nfs](https://github.com/adlogix/docker-machine-nfs)というのを使えばnfsでマウント出来るので多少速度の問題は改善されていました。ただこの方式だと

- マウントしているディレクトリのファイルの変更検知ができない
- docker toolboxに縛られる(きっと今後はdocker for macが主流になると思われるため)
- 多少速くなるがそれでも数万ファイルオーダーがあると使えないぐらい遅い場合がある
  
などがあり、できれば `docker-for-mac` を使いつつ早くする方法はないかと探していました。

実際docker-for-macの公式のフォーラムや、githubのissueには

- https://github.com/docker/for-mac/issues/77
- https://forums.docker.com/t/file-access-in-mounted-volumes-extremely-slow-cpu-bound/8076

などが挙げられているようです。で、この中でworkaroundとして [docker-sync](http://docker-sync.io/)というのが紹介されていてためしてみたところほぼラグもなく、docker-toolbox + nfsマウントよりもはるかに速くなったので手順を残しておきます。

## docker-sync

[docker-sync](http://docker-sync.io/)の仕組みとしては、 `docker` の `-v` で直接ホスト側のディレクトリをマウントするのではなく、`rsync`や`unison`の仕組みでホスト側のファイルをコンテナ側に転送しよう、というものです。

(詳細はわかりませんが、実際には、シンク用の名前付きボリュームを作成し、そこにホスト側のファイルを同期した上でそのボリュームをコンテナにマウントすることで実現しているようです）

### docker-syncのインストール
これは[公式wiki](https://github.com/EugenMayer/docker-sync/wiki)にあるとおりで、

本体のインストール

```
gem install docker-sync
brew install fswatch
```

同期手段に合わせて必要なミドルウェアをインストール。今回の場合は双方向(ホスト<=>コンテナ)の同期をしたかったので `unison` をインストールします。

```
brew install unison
```

これで`docker-sync`を使う準備は完了です。

## 試してみる

### docker-sync無しでまず実験

適当にコンテナを立てて、100Mのファイルをホスト側と共有してみます。 公式にもある通り、docker-composeを使うほうが簡単なのでそちらで試します。

```docker-compose.yml
version: '2'

services:
  app:
    image: busybox
    volumes:
      - .:/var/test
    command:
      tail -f /dev/null
```

カレントディレクトリをコンテナの`/var/test`にマウントして`tail`で待つだけ。

この状態で`docker-sync`使わない(とても遅い)環境はできたので、試してみましょう。

```shell-session
# up -dでもいいです。
docker-compose up
```

`up`した後別のターミナル(`up -d`の場合はそのまま) 下記コマンドでコンテナにアタッチ。コンテナ名はカレントディレクトリ名によって変わります。
今回の場合は`dstest_app_1`というコンテナ名です。

```
docker exec -it dstest_app_1 sh
```

その後コンテナの中に入り、マウントしたディレクトリに100MBのテストデータを作成します。

```
cd /var/test
/var/test # time dd if=/dev/zero of=speedtest bs=1024 count=102400
102400+0 records in
102400+0 records out
104857600 bytes (100.0MB) copied, 36.817946 seconds, 2.7MB/s
real    0m 36.81s
user    0m 0.16s
sys     0m 2.58s
/var/test # ls -lh speedtest
-rw-r--r--    1 root     root      100.0M Oct 23 05:28 speedtest
```

たった100Mのファイルを作成するのに**36秒**かかっています。ここ(`/var/test`)がマウントされているディレクトリのせいでこれだけ遅くなっているわけですね。
もちろんマウントしているのでホスト側にもこのファイルはできています。


ちなみに、マウントされていないコンテナ内のディレクトリで100Mのファイルを作成すると、

```
/var/test # cd /tmp
/tmp #  time dd if=/dev/zero of=speedtest bs=1024 count=102400
102400+0 records in
102400+0 records out
104857600 bytes (100.0MB) copied, 0.215219 seconds, 464.6MB/s
real    0m 0.21s
user    0m 0.01s
sys     0m 0.20s
```

`/tmp` で試すと約**0.2秒!**です。180倍ぐらい遅いみたいですね。

### docker-syncを使って実験

では、docker-syncをつかってみます。

docker-sync自体の設定は `docker-sync.yml` に書いていきます。で、docker-syncはdocker-composeでの設定上書きの機能をうまく使っていて既存の `docker-compose.yml`　はそのままに、それに対して設定を上書きするyml(デフォルトでは `docker-compose-dev.yml`)を作ることで実現しています。（もちろん、そのまま`docker-compose.yml`自体に設定してしまっても動きます。)


まずdocker-syncの設定

```docker-sync.yml
version: '2'

syncs:
  sync-volume:
    src: '.'
```

**2017/03/15 追記** docker-syncのバージョン `0.2.0` 以降から `docker-sync.yml` の書式に色々変更があったようです。 　詳しくは @smith_30 からのコメントと、そのリンク先参照してください。

**2018/08/20 追記** @drmatio から指摘頂きまして、docker-syncのバージョン `0.3.0` 以降から `dest` は不要になったそうです。
たしかに、docker-compose側のvolumesでマウント先指定していたので二度手間だったんですね。

公式のwikiには除外ファイルの設定など色々オプションが載ってますので、参考にしてみてください。ここでは最小限の設定で、
ホスト側のディレクトリ(`src`)、 ~~それをコンテナ内のどのパスにするか?(`dest`)~~ (0.3.0からdestは不要になった),それらをどの同期方式で行うか？(`sync_strategy`)を指定しています。

またここで書いている`sync-volume`は任意の名称でよいのですが、これが実際にdockerの名前付きボリュームとして作られるボリューム名称になります。

```
% docker volume ls | grep sync-volume
local               sync-volume
```

次に、docker-compose.ymlの上書き設定

```docker-compose-dev.yml
version: '2'

volumes:
  sync-volume:
    external: true

services:
  app:
    volumes:
      - sync-volume:/var/test
```

単純ですね。 `docker-sync`側で自動で作ってくれるボリュームを`external`として宣言しておき、それをカレントディレクトリでなく`sync-volume:/var/test`としてマウントするように設定します。

これで設定は完了なので実行してみます。

実行するには`docker-sync-stack start`で行います。が、実体としては、

```
docker-sync start # docker-syncで同期開始
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up
```

相当のようですので、こちらでもいいと思います。

`docker-compose`は `-f ymlファイル` を出てきた順番に後勝ちで設定を上書きしていくので、

- docker-compose.yml
- docker-compose-dev.yml

の順番で指定することで、volumeの設定 `.:/var/test` を `sync-volume:/var/test` に上書きしてくれているわけです。

とりあえず、ここでは、

```
docker-sync-stack start
```

で開始します。その後同様に、コンテナ側でホスト側に同期するディレクトリに100Mのファイルを作成します。

```
/ # cd /var/test
/var/test # time dd if=/dev/zero of=speedtest bs=1024 count=102400
102400+0 records in
102400+0 records out
104857600 bytes (100.0MB) copied, 0.189765 seconds, 527.0MB/s
real    0m 0.19s
user    0m 0.00s
sys     0m 0.19s
/var/test #
```

うん。マウントしていないディレクトリで試したときと同じぐらいの速度でファイルができました。
コンソールを眺めていると分かるのですが、上記コマンド打った瞬間に同期が走り、即座にホスト側にも反映されていました。

また、上記の実験は`コンテナ側->ホスト側`の同期でしたが、逆向きの同期もほぼラグなしに出来るようです。

## まとめ

これだけ速くなるので、`docker for mac`のマウントの遅さに辟易している方は、`docker for mac`自体がパフォーマンス改善してくれるまで、`docker-sync`を使ってみるのはおおいにアリだと思います。

別記事で[dockerでrails5環境構築](http://qiita.com/pocari/items/456052a291381895f8b3)というのを書いていて、そこでもマウントの問題で色々困ったことがありましたが、docker-syncを使う回避策も良さそうです。

http://qiita.com/pocari/items/456052a291381895f8b3#%E3%83%AA%E3%83%AD%E3%83%BC%E3%83%89%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C

ちなみに、ファイル数が多くなったらどうなるんだ？という疑問があるかもしれませんが、 [docker-sync](http://docker-sync.io/)によると、

> efficient in its way to watch for file changes - even for 12k+ files it will not eat up your CPU

とあるので、万単位でも問題無いようですね。

実際僕はフロント側約6千ファイル、バックエンド側約2万ファイルのプロジェクトをフロント、バックの二箇所をそれぞれ`docker-sync`で同期してみましたが、ほとんどローカルで開発しているのと大差無いぐらいの速度がでているので大量のファイルがあっても非常に効果的だと思います。

上記で使ったサンプルは、
https://github.com/pocari/docker-sync-sample
に置いています。