mutagenのsyncとdocker-composeを連携させて使う

# はじめに

最近[Docker for MacのMutagen-based cachingを使う](https://qiita.com/nanasess/items/16ab9274c34bdc34e567)という記事が話題になっていたが、mutagen単体でDockerコンテナと連携するやり方が英語の記事を含めてもなかなか見つけられなかったのでまとめておく。

なお、本記事の方法ではMutagen-based cachengは利用しないため、Dockerはstable版のままでよい（上記記事の設定は試していないため動作の違い等は不明）。

またここで扱っているのはmutagenの`sync`で`forward`については[こちらの記事](https://qiita.com/suin/items/aa2e80dce5714ed01670)に載っている。

# 環境
```shell
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G5033
$ docker -v
Docker version 19.03.8, build afacb8b
$ docker-compose -v
docker-compose version 1.25.5, build 8a1c60f6
```

# 準備

## 1. インストール

  [公式](https://mutagen.io/documentation/introduction/installation)に書いてある通り。投稿時のバージョンは0.11.4。

```shell
$ brew install mutagen-io/mutagen/mutagen
$ mutagen version
0.11.4
```

## 2. デーモン起動

```shell
$ mutagen daemon start
```

## 3. コンテナ起動

テスト用に用意したdocker-compose.ymlは以下。

```docker-compose.yml
version: '3.7'
services:
  web:
    image: php:7.4-apache
    container_name: web
    ports:
      - 80:80
  db:
    image: mysql:8.0
    container_name: db
    environment:
      MYSQL_ROOT_PASSWORD: root
```

```shell
$ docker-compose up -d
```

# 手動で使う

## 1. セッションの作成

```shell
$ mutagen sync create --name=mutagen-test --default-file-mode-beta=644 . docker://web/var/www/html
```

こちらのコマンドで`mutagen-test`という名前でカレントディレクトリ（**alpha**）の内容をwebコンテナの/var/www/html（**beta**）と同期するセッションを作成できる。なぜ`source`と`dest`ではなく`alpha`と`beta`なのかと疑問だったが、mutagenは双方向に同期できるための[ようだ](https://mutagen.io/documentation/synchronization#design-and-architecture)。
`--default-file-mode-beta=644`はbeta（コンテナ）側のファイルパーミッションを`644`にする設定。mutagenのデフォルトではパーミッションがディレクトリ`700`、ファイル`600`になる（[参考](https://mutagen.io/documentation/synchronization/permissions#permissions)）ので、ここで指定して（もしくは`--default-owner-beta=www-data`として）おかないと、今回のようにapacheから参照したい場合にパーミッションエラーになる。

セッションを作成した時点で同期が始まる。これまで使っていてファイルが消えたりしたことはないが、万が一に備えてバージョン管理されている状態で行った方がよい。

## 2. 同期の状況の確認

- 実行時点

```shell
$ mutagen sync list
--------------------------------------------------------------------------------
Name: mutage-test
Identifier: sync_pPfc22rlD8RcEDldZ9CbMTJzTnRszZHfokhkZtopw7a
Alpha:
        URL: /tmp/mutagen
        Connection state: Connected
Beta:
        URL: docker://web/var/www/html
                DOCKER_HOST=
                DOCKER_TLS_VERIFY=
                DOCKER_CERT_PATH=
        Connection state: Connected
Status: Watching for changes
```

- リアルタイム

```shell
$ mutagen sync monitor
Name: mutagen-test
Identifier: sync_pPfc22rlD8RcEDldZ9CbMTJzTnRszZHfokhkZtopw7a
Labels: None
Alpha: /tmp/mutagen
Beta: docker://web/var/www/html
        DOCKER_HOST=
        DOCKER_TLS_VERIFY=
        DOCKER_CERT_PATH=
Status: Watching for changes                                                    
```
セッション作成直後の最初の同期中にmonitorを見ていると`Status: Staging files on beta: 41% (9191/22567)`などといった状態になり、これが`Status: Watching for changes`になったら同期完了。
ファイルの量やマシンスペックにもよると思われるが、上記の22567ファイルの規模（今回のものとは別のプロジェクト）で20秒程度だった。monitorは同期が完了しても勝手には終わらないので^Cで抜ける。

## 3. 動作確認

- 新規作成

```shell
$ ls
docker-compose.yml
$ echo '<?php echo "It works!";' > index.php
$ docker-compose exec web ls -la
total 20
drwxrwxrwx 1 www-data www-data 4096 Jun  4 05:29 .
drwxr-xr-x 1 root     root     4096 May 15 12:41 ..
-rw-r--r-- 1 root     root      206 Jun  4 05:28 docker-compose.yml
-rw-r--r-- 1 root     root       24 Jun  4 05:29 index.php
$ curl localhost
It works!
```

- 編集

```shell
$ echo '<?php echo "It works fine!";' > index.php
$ curl localhost
It works fine!
```

ちゃんと同期されているようだ。

今回はサイズも小さいので一瞬だが、実プロジェクトでは数秒のラグはある（コンテナでファイル自動生成などをしてすぐにホストで`git status`で見るとまだ反映されていなかったりする）。

## 4. セッションの終了
```shell
$ mutagen sync terminate mutagen-test
$ mutagen sync list
Error: no matching sessions exist
```

## 5. 手動の問題点

最初はこれでしばらく使っていたが、例えばセッション実行中にコンテナを落とすと、`Status: [Errored] Connecting to beta`となり同期エラーになったり、そこから`docker-compose up -d`で再度コンテナを起動しても、`Status: Halted due to one-sided root emptying`となって同期はできずセッションを作り直さなければならないという問題があった。

先述のオーナーやパーミッションは[設定ファイル](https://mutagen.io/documentation/introduction/configuration)に書くこともできるが、コンテナとの連携も合わせて一括管理したいなあと思っていたところ、次の`mutagen project`を使う方法を見つけた。

# mutagen projectを使う

mutagenには[project](https://mutagen.io/documentation/projects)という機能があり、セッションの作成や終了といったライフサイクルと任意のコマンドを組み合わせて設定ファイルで管理し実行できる。

公式の[examples](https://github.com/mutagen-io/mutagen/blob/master/examples/docker/web-go/mutagen.yml)がわかりやすい。

今回は簡略化した以下のmutagen.ymlを使用する。

```mutagen.yml
beforeCreate:
  - docker-compose up -d

afterTerminate:
  - docker-compose down

sync:
  defaults:
    ignore:
      vcs: true
  mutage-test:
    alpha: "."
    beta: "docker://web/var/www/html"
    mode: "two-way-resolved"
    configurationAlpha:
      permissions:
        defaultFileMode: 644
        defaultDirectoryMode: 755
    configurationBeta:
      permissions:
        defaultOwner: www-data
        defaultGroup: www-data
        defaultFileMode: 644
        defaultDirectoryMode: 755
```

`beforeCreate`でセッション作成前にコンテナを起動、`afterTerminate`でセッション終了後にコンテナも終了する。

また、`sync`の項目にセッションの設定を書いておける。
ここではalphaのパーミッションも設定しているが、これはフレームワークなどのファイル自動生成やcomposerやnpmでのライブラリのインストールをコンテナ上で行った場合に`700`や`600`でホストに同期されるのを防ぐためである。

[公式](https://github.com/mutagen-io/mutagen/blob/master/examples/docker/web-go/mutagen.yml#L9)では`afterCreate`で同期完了までの[wait](https://github.com/mutagen-io/mutagen/blob/master/examples/docker/web-go/frontend/wait-for-initial-build.sh)を入れている。同期が速いといっても多少のラグはあるので、実プロジェクトで採用する際は真似しておくと、同期完了前にアクセスしてファイルが見つからないなどのエラーを避けられる。

`docker-compose.yml`と同階層に置いてバージョン管理しておく。

## 1. セッションの開始

```shell
$ mutagen project start
Started Mutagen daemon in background (terminate with "mutagen daemon stop")
> docker-compose up -d
Creating network "mutagen_default" with the default driver
Creating web ... done
Creating db  ... done
Created session sync_3bwd2dlhwaFrNNeUNRe4WYegNxOdPjEj3swQ8Xx1Hz6
```

セッション開始前にコンテナが起動しているのが確認できる。デーモンが起動していなかった場合はそっちも起動してくれて便利。

セッション実行中は`mutagen.yml.lock`というファイルができるので`.gitignore`に追加しておくとよい。

## 2. セッションの終了

```shell
$ mutagen project terminate
> docker-compose down
Stopping web ... done
Stopping db  ... done
Removing web ... done
Removing db  ... done
Removing network mutagen_default
```

## 3. コマンドの実行

また、composerやnpmなどのscriptsと同様に任意のコマンドを定義できるようになっている。以下を設定ファイルに追加する。

```mutagen.yml
commands:
  repl: docker-compose exec web php -a
  db: docker-compose exec db mysql -uroot -p
```

```shell
$ mutagen project start # セッションを終了していない場合は不要
$ mutagen project run repl
Interactive shell

php > echo 'hello';
hello
php > ^D
$ mutagen project run db
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.20 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```

# おわりに

`mutagen project`を利用することで、mutagenとdocker-composeを連携できて便利だった。
現状そこまで不満もないのでDockerのMutagen-based cachingがstableになるまではこの方法を使おうと思う。


# 参考
- [DXを大幅に低下させるDocker for Macを捨ててMac最速のDocker環境を手に入れる](https://qiita.com/yuki_ycino/items/cb21cf91a39ddd61f484) ( こちらの記事でmutagenなるものを知りました。ありがとうございます。)
- [公式ドキュメント](https://mutagen.io/documentation/introduction)
- [Improving performance for Docker on Mac computers when using named volumes](https://medium.com/netresearch/improving-performance-for-docker-on-mac-computers-when-using-named-volumes-55580efcbf68)
- [Speeding Up Docker Development on the Mac](https://blog.rocketinsights.com/speeding-up-docker-development-on-the-mac/)