Laravel を使用していて Docker コンテナの起動に失敗する場合に composer install をするには docker-compose run を実行する

# TL;DR

コンテナを `docker-compose up` で起動せずに特定のコマンドを実行するには `docker exec` ではなく `docker-compose run <container-name> <command>` を実行する。

# 発生したエラー

Docker 内で Laravel を使用していたところ、Docker コンテナの起動に失敗する現象が発生しました。

```bash
$ docker-compose up
# 中略
my_container | 
my_container | Warning: require(/var/www/my-app/vendor/autoload.php): failed to open stream: No such file or directory in /var/www/my-app/artisan on line 18
my_container | 
my_container | Fatal error: require(): Failed opening required '/var/www/my-app/vendor/autoload.php' (include_path='.:/usr/local/lib/php') in /var/www/my-app/artisan on line 18
my_container exited with code 255
```

これは `composer install` が実行されていないために起きるエラーですが、この記事ではこちらを解決していきます。

# 前提知識：Docker とボリュームのマウントについて

Docker を使用する場合、基本的にソースコードはコンテナの外に配置するようになっています。そうすることで、コンテナの中に入ることなく vim や vscode などでソースコードを編集できます。さて、これをコンテナ内と結びつけるためには docker-compose にボリュームという項目を追加し、リポジトリをマウントすることができます。

```yaml:docker-compose.yml
services:
  my_container:
    # 中略
    volumes:
      - type: bind
        source: ./my-app
        target: /var/www/my-app
```

さて、ここで重要なのは、docker-compose 内では（つまりコンテナの起動時には）ボリュームをマウントできる一方で、Dockerfile では（つまりビルド時には）ローカルのフォルダをマウントできないということです。

https://logicoffee.hatenablog.com/entry/2018/06/21/123025

> Dockerfileにボリュームのマウントを設定する際は, ホストマシンのディレクトリを指定することができません

となると、ビルド時に `composer install` を行い、その結果をローカルファイルに反映するにはひと工夫が必要になってしまいます。これを避けるために、今回は `composer install` はビルド時ではなくコンテナの初回起動時に行うことにします。

# 解決方法

まず、コンテナをビルドします。

```bash
docker-compose build my_container
```

次に、`docker-compose up` でコンテナを初回起動する前に `docker-compose run` コマンドを使用して `composer install` を行います。

```bash
docker-compose run composer install
```

最後に、コンテナを起動します。`-d` を付けるとバックグラウンドで起動します。

```bash
docker-compose up
```

これで無事コンテナを起動することができました。恐らく `Python` (`pip install`) や `Node.js` (`npm install` や `yarn install`) でも同じ方法で対処できるかなと思います。

# もっと良い方法はないか？

個人開発であれば上記で問題ないこともあります。ですが、よりよい環境が作れないか考えてみましょう。

## 改善案1. docker-compose up 時に必ず composer install が走るようにする

```Dockerfile:Dockerfile
COPY ./my-startup-script.sh /usr/local/bin/my-startup-script.sh
RUN chmod +x /usr/local/bin/my-startup-script.sh
CMD ["my-startup-script.sh"]
```

```bash:my-startup-script.sh
composer install
apache2-foreground # サーバーを起動
```

これはお手軽で便利ですね。（スクリプトファイルを用意する代わりに `bash -c` でも行けると思います。）

https://sleepless-se.net/2018/05/26/docker%E3%81%A7%E8%A4%87%E6%95%B0cmd%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/

https://qiita.com/kon_yu/items/29e6e7ad1caa28dce498

もちろん `docker-compose.yml` に `command` を追加しても構いません。

https://qiita.com/ganyariya/items/3594ec682a3ccf917477

## 改善案2. vendor ディレクトリをマウントしない

vendor ディレクトリ（や、node_modules ディレクトリ等）をマウントしないようにすることで、誤ってホスト側で `composer install` してしまうようなミスを防ぐことができます。そもそもコンテナ内でしか役に立たないファイルなので、コンテナ外から見えないようにするのは確かに合理的に思えます。このテクニックは Volume Trick と呼ばれるそうです。

以下の記事のようにボリュームの中に別のボリュームを作成することで実現できるようです。

https://qiita.com/suin/items/e53eee56da23d476addc

または、単に空のボリュームを指定するだけでよいそうです。

```yaml:docker-compose.yml
services:
  my_container:
    # 中略
    volumes:
      - type: bind
        source: ./my-app
        target: /var/www/my-app
      - type: volume # ボリュームマウント
        target: /var/www/my-app/node_modules # 除外するディレクトリ
```

https://ja.linux-console.net/?p=7952#gsc.tab=0

さらに良いことに、これによりビルド時の vendor/ ディレクトリが引き継がれるそうです。

> 例えば自前で Dockerfile を用意し、Dockerfile 内部で npm install などを実行していた場合、イメージ内の node_modules を削除してしまうことになります。
>
> これを避けるためには以下のように名前付きボリュームを用意してあげることで node_modules が名前付きボリュームに格納され、ホストマシンに同期されることがなくなります

https://zenn.dev/tamanugi/articles/6f372d45b85c18

ただし、vscode を使用している場合は追加設定が必要になったりなど、自力でいくつかの問題を解決する必要があるかもしれません。その際は Volume Trick などで検索すると解決策が見つかるかもしれません。

https://zenn.dev/yumemi_inc/articles/3d327557af3554

# オチ

そもそも `docker compose up` でコンテナがエラー終了してしまうのがおかしいのでは？ という話なのですが、これは Dockerfile で `CMD` が誤って上書きされてしまっていたからというオチでした。上書きされる前のコマンドは Docker Hub 上の元のイメージを検索すると確認できました。