Docker for Windowsで共有ディレクトリを使って開発する

# はじめに
Docker for Windowsを使うとHyper-Vにより高いパフォーマンスでDockerが使えます。(要Windows10 pro)

そして、ソースコードの編集にはWindows側でVSCodeやGVimを使いたいというような場合には、Dockerの共有デイレクトリ機能を使って、WindowsとDockerコンテナの間でディレクトリを同期させることができます。

ただし、Docker for Windowsで共有ディレクトリを使う際には下記の問題があります。

1. 共有ディレクトリ内ではシンボリックリンクが使えない。
2. Host側でのファイルの変更がコンテナ側のiNotifyで通知されない。

下記に上記の問題に対する対応方法について記述します。

# シンボリックリンクが使えない対応
## 問題点
共有しているディレクトリ内で`npm install`や`yarn install`を実行すると、配下にnode_modulesを作成し、node_modules/binの配下にシンボリックリンクで実行ファイルをコピーしようとするので、installに失敗します。
## 対応
docker-composeを使ってシンボリックリンクが必要なディレクトリに対してlocalボリュームを指定します。
(例) windowsのsample-appというプロジェクトをコンテナと共有しつつ、その配下のnode_modulesのlocalボリュームにする対応。
localボリュームにすることで`yarn install`も速いです。

```yaml:docker-compose.yml
version: '3'
services:
  backend:
    image: Dockerイメージ名
    command: 起動コマンド名
    volumes:
       - 'C:\Users\takeshy\work\sample-app:/home/takeshy/sample-app'
       - /home/takeshy/sample-app/node_modules
    ports:
      - "3000:3000" <- webpack-dev-serverのportをlocalhostで使えるように。
```
`docker-compose up -d`を使って、docker-compose経由でdockerコンテナを立ち上げます。
ただ、Windows側でもyarn installを実行しないと、VSCode等でTypeScriptを開発している際にimportしているライブラリが見つかりませんの警告が出てしまいます。

# Host側でのファイルの変更がコンテナ側のiNotifyで通知されない
## 問題点
RailsやWebpack-dev-serverはファイルの変更を検知して、動的に変更があったファイルをロードし直してくれますが、Docker for Windowsの共有ディレクトリだと、コンテナ側のiNotifyが動かないため、Windows側でファイルを変更すると、コンテナ側もファイル自体は変更されるものの、RailsやWebpack-dev-serverがロードし直してくれません。（更新させるためには、RailsやWebpack-dev-serverの再起動が必要）
## 対応
いろいろあります。
A. エディタのリモートファイル編集機能を使う

B. コンテナ側でpollingで更新日時をチェックして、変更があればコンテナ側で該当ファイルをtouchする

C. ホスト側でファイルを変更した場合にコンテナ側に通知し、コンテナ側で該当ファイルをtouchする。

D. ホスト側でファイル変更を検知した場合に、Dockerコマンドで該当ファイルを同じモードでchmodする

VimもしくはViesual Studio Codeで開発している場合は、Cが断然おすすめです。ファイルのsaveと同時にコンテナ側に変更が反映されるようになりますし、CPU負荷がほぼないです。※DのツールはCのツール作成後に知りました。Dでいいかも。

###A. エディタのリモートファイル編集機能を使う

**Visual Studio Code**
https://marketplace.visualstudio.com/items?itemName=Kelvin.vscode-sshfs
**Vim**
http://vim.wikia.com/wiki/Editing_remote_files_via_scp_in_vim

上記で問題ない場合は一番お手軽でいいと思います。ただVimの場合、別途scpクライアントの導入が必要&常用しているエディターのプラグインがリモートファイル対応をしていなかったりと厳しい可能性があります。

###B.コンテナ側でpollingで更新日時をチェックして、変更があればコンテナ側で該当ファイルをtouchする

拙作の[touch_and_go](https://s3-ap-northeast-1.amazonaws.com/takesy-work/touch_and_go)を使う。(ソース:https://github.com/takeshy/touch_and_go)
touch_and_goはコンテナ側の対象ディレクトリ以下にあるファイルをpollingで指定された間隔ごとに更新日時をチェックして、前にチェックした時と違っていた場合は、そのファイルに対して更新日時をそのままでアクセス時間を実行時にセットすることで強制的にiNotifyを呼び出させるツールです。
設定

```json:config.json
{
  "watchers": [
    {
      "directory": "/home/ubuntu/sample",
      "excludes": ["node_modules"]
    }
  ]
}
```
watchers以下に監視したいDirectoryごとにdirectoryとexcludes(任意)を指定します。
directoryが/で始まらない場合は、実行時のカレントディレクトリからの相対パスになります。excludesは、監視対象から外したいディレクトリおよびファイル名を指定します。

#### 実行

コンテナ側で下記を実行

```shell
touch_and_go -c config.json -i 6000 &
```

-c 設定ファイルのパス(default: config.json)
-i はポーリングの間隔(単位はミリ秒 default: 3000)

対象Directory配下のすべてのファイルの更新時間をチェックするので、対象ファイルが多い場合は更新時間を長めにとらないと負荷が高くなります。
ただし長めに取ると、変更したのに更新がなかなかされないということになります。
また、Pollingは何もしていない時でも常にDiskにアクセスしていて、システム負荷がかかるのでDiskにもやさしくないです。

###C.ホスト側でファイルを変更した場合にコンテナ側に通知し、コンテナ側で該当ファイルをtouchする。

拙作の[go-touch](https://s3-ap-northeast-1.amazonaws.com/takesy-work/go-touch)を使う。(ソース:https://github.com/takeshy/go-touch)
go-touchはtcp(デフォルトは7650ポート)で待ち受けて、クライアントから変更があったファイル名を受信した場合に、そのファイルに対して更新日時をそのままでアクセス時間を実行時にセットすることで強制的にiNotifyを呼び出させるツールです。そのためHost側(Windows)のプログラムも必要です。
また、docker runの際にport forwading(デフォルトは7650)の設定も必要です。
(例)

```
docker run -p 7650:7650 -v ~/work/PROJECT:/home/hogohoge/PROJECT IMAGENAME START_PROG
```
#### 実行

コンテナ側で下記を実行

```shell
go-touch -p 7650 -h 0.0.0.0 &
```

-p ポート番号(default: 7650)
-h 待ち受けるIP(default:0.0.0.0 ※すべて受信)

Vim用とVisual Studio Code用にSaveした際にSaveしたファイル名を送信するプログラムを作成しました。

##### vimの場合

vimrcに下記を追記

```vimscript:_vimrc
function! SendSavedFile() abort
  let removePath = "/work"
  let appendPath = ""
  let port = 7650
  if match(expand('%:p'), expand('~') . removePath) == 0
    let filePath = substitute(expand('%:p'), expand('~') . removePath, appendPath, '')
    let channel = ch_open('localhost:' . port)
    call ch_sendraw(channel, filePath . "\n")
  endif
endfunction
autocmd BufWritePost * :call SendSavedFile()
```

上記のスクリプトの変数を下記を元に書き換えてください。

- removePath ...  Host側の$HOME以降のPathのうち、コンテナ側のPathから省きたい場合に指定 <br/>
**例** もしHost側(Windows)が/Users/USERNME/work/PROJECTでコンテナ側は/home/USERNAME/PROJECTだった場合は、removePathに/workを指定します
- appendPath ... コンテナ側に$HOME以降に別途付与したいパスがある場合に指定 <br/>
**例** もしHost側(Windows)が/Users/USERNAME/PROJECT でコンテナナ側が/home/USERNAME/app/PROJECTだった場合はappendPathに/appを指定します
- port ... go-touchのポート番号を指定

##### Visual Studio Codeの場合

extensionでgo-touch-clientを検索。
extensionをinstall後、Visual Studio Code再起動後に下記の設定を行う。

- go-touch-client.removePath ...  Host側の$HOME以降のPathのうち、コンテナ側のPathから省きたい場合に指定 <br/>
**例** もしHost側(Windows)が/Users/USERNME/work/PROJECTでコンテナ側は/home/USERNAME/PROJECTだった場合は、removePathに/workを指定します
- go-touch-client.appendPath ... コンテナ側に$HOME以降に別途付与したいパスがある場合に指定 <br/>
**例** もしHost側(Windows)が/Users/USERNAME/PROJECT でコンテナナ側が/home/USERNAME/app/PROJECTだった場合はappendPathに/appを指定します
- go-touch-client.port ... go-touchのポート番号を指定

### D. ホスト側でファイル変更を検知した場合に、Dockerコマンドで該当ファイルを同じモードでchmodする

pythonでできたツールが公開されていました(B,Cの開発後知りました..)
https://pypi.org/project/docker-windows-volume-watcher/

ホスト側(Windows)で ReadDirectoryChangesWというAPIを使ってファイルの変更を検知し、変更があった場合はDockerのAPI経由でchmodを実行することでコンテナ側でファイル変更を検知させるようになっています。Pollingではないので、システム負荷的にもあまり問題なく、コンテナ側はツールをinstallする必要がないので、Windows側でPythonをinstallさえすれば、一番楽な気がします。

#まとめ
Docker for Windows(要Windows 10pro)のDockerのパフォーマンスがMacに比べて格段にいいので、Windowsに乗り換えましたが、上記等の対応が必要なこともあり、Windowsの必要性がない場合は、素直にLinuxをinstallするほうがいいと思います。
ただ、IEの検証が簡単にできたり、OfficeやゲームなどのPC用のソフトが使えたり、リモートデスクトップ機能(要Windows 10pro)で、MacやiPadからも接続して開発できるというメリットがあるので、しばらくはWindowsを使っていこうと思います。