Docker x Laravel めちゃくちゃ遅い Docker for Mac を爆速化する

## Docker for Mac 遅い問題

- [Macのdockerが遅いストレスから解放されよう](https://qiita.com/ysKey2/items/346c429ac8dfa0aed892)
- [DXを大幅に低下させるDocker for Macを捨ててMac最速のDocker環境を手に入れる](https://qiita.com/yuki_ycino/items/cb21cf91a39ddd61f484)
- [MacのDockerが遅い原因と対処方法（公式）](https://wonwon-eater.com/mac-docker-performance/)
- [Docker For Macが遅い：対策の実験](https://qiita.com/FattyRabbit/items/bfb5a9a321718b7f2756)
- [Docker for Mac遅すぎる問題の解決](https://tech.ga-tech.co.jp/entry/2019/10/docker-vagrant)
- [Docker for Macでマウントしたvolumeの遅さに対処する](https://www.to-mega-therion.net/docker/docker-mounted-volume-slow)
- [【JavaScript】Docker上でのnpm/yarnの操作を10倍早くする方法](https://tkkm.tokyo/post-495)
- [Docker-compose: node_modules not present in a volume after npm install succeeds](https://stackoverflow.com/questions/30043872/docker-compose-node-modules-not-present-in-a-volume-after-npm-install-succeeds)

たくさんの記事が出ている通り、Docker for Macが遅いというのはみんなが思うところ...
Dockerの勉強を始めて1年が経ち、今まで目を逸らしていた問題にようやく重い腰をあげてこの問題に取り組む時が来た...！

## Docker for Mac 遅い原因

大量のファイルが変更され、ホストとコンテナ間でファイル同期処理が行われるため。
(Docker for Macの速度がアップデートで勝手に改善してくれればいいな...と願って待ってました...。)

## Docker for Mac 遅い問題の解決策

- 案1. docker-syncを使う
- 案2. 仮想マシンにLinuxを入れて、docker-composeを使う
- 案3. バインドマウントオプションのcached, delegatedを使う
- 案4. ボリュームマウントを使う

上記の参考記事は大体この4つの案のどれかで対応してました。
すべて試した訳じゃないのでハッキリしたことは言えないですが、どの方法もメリットデメリットはあるので許容できる方法を使うのが良いかと思います。おそらくどの方法も高速化は見込めると思います。

### 案1. docker-syncを使う

https://github.com/EugenMayer/docker-sync

docker-syncの仕組みとしては、
通常のバインドマウントでホスト側のディレクトリをコンテナ側へマウントするのではなく、rsyncやunisonといった仕組みでホスト側のファイルをコンテナ側に転送するようです。

デメリットとしては、docker-syncを導入しなくてはいけないというそのもの。
構成が大きく変わるし、学習コストが跳ね上がる気がする。

### 案2. 仮想マシンにLinuxを入れて、docker-composeを使う

Docker for Macが遅いんだから、Linuxを通して使っちゃえという案。
既存のdocker-compose.yml等の変更が一番抑えられるし、本番により近い構成でとても良い方法に思える。

デメリットとしては、私はMacで開発したいんだ。

### 案3. バインドマウントオプションのcached, delegatedを使う

https://docs.docker.com/docker-for-mac/osxfs-caching/

Dockerの17.04以降に追加されたオプション cached, delegated は読み込みや書き込みの一貫性を担保しない代わりにパフォーマンスが向上されるというものです。

デメリットとしては、あれれー？コードの変更が反映されないんだけど？みたいなことが多発しそう。。

### 案4. ボリュームマウントを使う

- [【JavaScript】Docker上でのnpm/yarnの操作を10倍早くする方法](https://tkkm.tokyo/post-495)
- [Docker-compose: node_modules not present in a volume after npm install succeeds](https://stackoverflow.com/questions/30043872/docker-compose-node-modules-not-present-in-a-volume-after-npm-install-succeeds)

vendorやnode_modulesのデータを名前付きボリュームに格納して、ホスト側とコンテナ側と分離して管理する案。
同期処理が発生しないので、その分速くなる。

デメリットとしてはホスト側でも`composer install`しなければIDEの自動補完等の恩恵が受けられなくなる。

### どの案にするか？

個人的には案2と案4で迷うところですが、今回は案4を試したいと思います。

## Laravelで大量のファイルの読み書きが多く発生するディレクトリは？

下記の3つです。(他にもあったら教えてください)

- vendor (composer installで生成されるディレクトリ)
- node_modules (npm, yarn installで生成されるディレクトリ)
- storage (フレームワークのキャッシュファイル等が生成されるディレクトリ)

今回は、vendorとnode_modulesに高速化対策したいと思います。
storageは別の話になるので別記事で対応します。

## 案4を採用した結果、当社比20倍の高速化に成功！

結果から言うと大満足です！
(まだ実際にこの案で業務利用したことないので要検証ですが...)

```shell-session
$ time composer install
real	2m40.358s => 0m8.383s
user	0m4.003s => 0m1.849s
sys	0m8.209s => 0m2.526s
```

Composerのインストールは152秒の短縮、20倍の高速化に成功！！！

```shell-session
$ time npm install
real	2m 35.33s => 0m 20.54s 
user	0m 44.96s => 0m 19.64s
sys	0m 19.44s => 0m 7.56s
```

npmのインストールは135秒の短縮、7倍の高速化に成功！！！

## 案4. ボリュームマウントを使う 手順

本題のDocker高速化の手順です。

### 環境

- [docker-laravel🐳 | GitHub](https://github.com/ucan-lab/docker-laravel)
  - PHP 7.4.4
  - Laravel 8.8.0
- [最強のLaravel開発環境をDockerを使って構築する【新編集版】](https://qiita.com/ucan-lab/items/5fc1281cd8076c8ac9f4)

上記の環境を使用してます。
StarやLGTMもらえると記事を書くモチベになるので良い記事だなと思っていただければぜひよろしくお願いします！

### 前提

Laravelのインストールまで完了している状態

### docker-compose.yml を編集

下記、コメントした4行を追記してください。

```yml:docker-compose.yml
version: "3.8"
volumes:
  php-fpm-socket:
  db-store:
  vendor-store: # add
  node_modules-store: # add
services:
  app:
    build: ./infra/docker/php
    volumes:
      - php-fpm-socket:/var/run/php-fpm
      - ./backend:/work/backend
      - vendor-store:/work/backend/vendor # add

  web:
    build: ./infra/docker/nginx
    ports:
      - 80:80
    volumes:
      - php-fpm-socket:/var/run/php-fpm
      - ./backend:/work/backend
      - node_modules-store:/work/backend/node_modules # add

  db:
    build: ./infra/docker/mysql
    ports:
      - 3306:3306
    volumes:
      - db-store:/var/lib/mysql
```

設定内容を反映させます。

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

設定内容を反映させるとコンテナ側の `vendor` ディレクトリの中身はなくなってるはずです。
ホスト側の `vendor` の中身はあってもいいし、中身を削除しちゃってもいいです。
それぞれ独立したのでホスト側の `vendor` の中身がなくなっても動きます。

あと若干のコンテナの起動が遅くなってる気はします。(私だけかもしれない)

一応、下記のコマンドでファイルがどうなってるか確認しましょう。
node_modulesは最初は元々ファイルないのでホスト側もコンテナ側も空ですけど...

```
$ ls -l ./backend/vendor
$ docker-compose exec app ls -l ./vendor

$ ls -l ./backend/node_modules
$ docker-compose exec web ls -l ./node_modules
```

下記のコマンドを実行して依存パッケージをインストールします。
(爆速になってるはずです)

```
$ docker-compose exec app composer install
```

Nodeの場合は下記のコマンドになります。

```
$ docker-compose exec web npm install
```

このままだと新しくライブラリを追加してもホスト側の `vendor` ディレクトリは更新されないので、下記のコマンドを使用して更新します。

```
$ docker-compose run --rm -v $(pwd)/backend:/code -w /code app composer install
$ docker-compose run --rm -v $(pwd)/backend:/code -w /code web npm install
```

- `docker-compose run` サービスに対して1回限りのコマンドを実行します
  - https://docs.docker.com/compose/reference/run
- `--rm` コンテナ実行後に削除
- `-v` バインドボリュームを指定
- `-w` コンテナ内のワーキング・ディレクトリを指定

バインドボリュームでホスト側のディレクトリに依存ライブラリをインストールしています。
(これはちょっと時間かかります)

ホスト側、コンテナ側のそれぞれに依存ライブラリのインストールする必要があるので、総合的な所要時間は増えてしまったように思う。
ホスト側はIDEの自動補完ができればいいので、そう頻度高く更新しなくても良いのかなと思って妥協してます。

## Makefile を作る

長いのでMakefileにすると便利です。

```makefile:Makefile
composer-install:
	@make composer-install-for-container
	@make composer-install-for-host
composer-install-for-container:
	docker-compose exec app composer install
composer-install-for-host:
	docker-compose run --rm -v $$(pwd)/backend:/code -w /code app composer install
```

Makefileでは変数展開は実行前に行われてしまうので `$$(pwd)` のように `$$` 2個付けると良いです。

## 最後に

記事中にも言いましたが、あまり検証していないのでこの記事を参考にお試しいただけた方はコメントいただけると嬉しいです！