DockerによるWebシステム開発をWindows、Mac(Linux)混在チームで実現する方法 

# 目的

- DockerベースのWeb開発を行う際に開発者のPCがWindows, Mac(Linux)で混在している場合に生じる問題を回避する

## 動機

- 以前、自分がMac、他のエンジニアたちがWindowsという構成の新規開発のチームを率いたときにこの問題を解決する必要が生じた

## 問題点の確認

- Dockerベースで開発するシステムなのにDockerの外で実行されるビルド用などのスクリプトが混在している
- Mac PCに慣れた開発者ほど無自覚にそうしてしまいがち
- つまり、Dockerの中で動かすプログラムが仮にWindows機上でうまく動くように配慮されていたとしても、ビルド用スクリプトが直接ホスト上で実行される構成となっていればにそこでコケる

## 運用方法

### [1] Windowsを捨てる

チームの開発PCの構成がMac主体でWindows機の影響を無視できる場合、Windows対応を捨ててしまうというのが何も考えなくて良くなるためある意味最小の解決策となります。

当然ながらWindows機では開発環境の構築や運用ができないソースツリーとなるため、Windows機上で先にUbuntuのWSL2ホストをインストールし、その中でソースツリーを展開してDockerを起動します。

つまり、Mac, Linux PCの場合にシステムはこのような構成ですが、

> PC -> Dockerコンテナ

Windows PCではこのような構成になります。

> PC -> Ubuntu(WSL2ホスト) -> Dockerコンテナ

つまりWindows PCにおいてもソースツリーを扱う主体がLinux(WSL2)となるため、MacやLinux PCとも理論上差のない形で開発環境を運用できることになります。

こういった運用については既に記事を執筆済みです。

- Windows環境(WSL2)のDockerを爆速化する方法
  - https://qiita.com/kanryu/items/c4a975f0e5b23d8bf039

こちらの記事では軽量LinuxディストロのAlpine Linuxを使って構築していますが、GitHubをプロジェクト管理ツールとしている場合、`GitHub cli`がAlpineでは動作しないためUbuntuの方を推奨します。

### [2] ビルド用スクリプト等も含め、全てのスクリプト、プログラムの実行をDocker内で統一する(本記事)

システムの開発環境の構築やシステムのビルド時に主に必要になってくる環境依存の作業とは、以下が挙げられます。

- pypi(Python), composer(PHP), npm(Node.js)などのシステムのプログラムが必要としている外部パッケージの導入
- JavaScript, CSS等、システムのプログラムが採用しているプログラミング言語(以下メイン言語)の外でHTML5を構成する諸要素を個別にビルドする

これらの作業をチーム各人のPC環境で直接実行するような実装を行うと環境依存の問題が発生します。
したがってこの問題を解決するにはこういった作業もDocker内での実行に統一すべきです。

Dockerでのビルド等のコマンド実行には主に２つの方法があります。

## Dockerfileに記述した初期構築スクリプトの問題


一般的にDockerによる開発ではDockerfile、もしくはdocker-compose.ymlに記述された設定に従って各コンテナを定義し、そのコンテナは起動が完了すると特定のポートを開いて待機状態になるはずです。そういったDockerfileは最後のコマンドに `EXPOSE` 命令が使われています。


以下は実例として `docker-laravel-standard` を例に取ります。

- docker-laravel-standard
  - https://github.com/kanryu/docker-laravel-standard


例： https://github.com/kanryu/docker-laravel-standard/blob/main/docker/php/Dockerfile

```Dockerfile
FROM php:8.1-fpm
COPY php.ini /usr/local/etc/php/

RUN apt-get -y update \
  && apt-get install -y zlib1g-dev zip mariadb-client vim curl gnupg \
  libzip-dev libfreetype6-dev libjpeg62-turbo-dev libpng-dev libmcrypt-dev \
  && docker-php-ext-install zip pdo_mysql gd iconv exif
RUN apt-get install libyaml-dev -y
RUN pecl install yaml && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/ext-yaml.ini && docker-php-ext-enable yaml
RUN pecl install xdebug

#node.js install
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
RUN apt-get install -y nodejs
RUN npm install npm@latest -g

#Composer install
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/var/www/laravel-app/vendor/bin

WORKDIR /var/www

RUN composer global require "laravel/installer"
EXPOSE 5173
```

この `Dockerfile` では PHP、node.js、composerの初期設定が行われていますが、composer準拠のPHPパッケージのインストール `composer install` は実行されません。

Dockerfile内に以下のように書けば実際に実行してくれるのですが、これでは実運用に問題があります。

```
RUN composer install
```

なぜかというとDockerfileの各RUNコマンドは `docker build` 実行時にただ１度だけ実行され、2回目以降Dockerコンテナを起動した際などには実行されないためです。

システム開発の成長過程で package.json(node.js) composer.json(PHP)などのパッケージ設定ファイルを更新し、パッケージの構成を変更することはままあることですが、システムのDockerコンテナを破棄して再構築しなければ `composer install` を実行してくれないということになります。それは不便ですね。


## ビルドスクリプトのDocker化

### [1] CMDの活用

Dockerコンテナにはもう一つの実行モードがあり、それは `CMD` コマンドを書いた場合です。
この場合、CMDに書かれたコマンドの実行が終了すると、Dockerコンテナごとクローズされます。

このコマンド化された Dockerコンテナは、他の通常のDockerコンテナと同じように扱われ、コンテナ群全体の起動時に毎回実行されることになります。

例えば、 `composer install` を実行する CMD型 Dockerコンテナは docker-compose.yml 上でこのように書けます。

例: https://github.com/kanryu/docker-laravel-standard/blob/main/docker-compose.yml

```yaml
  build-app:
    container_name: docker_laravel_build_app
    build: ./docker/php
    command:
      - /bin/sh
      - -c
      - |
        composer install
        npm install
        npm run build
    volumes:
      - ./laravel-app:/var/www
      - ./docker/php/dev/php-xdebug.ini:/usr/local/etc/php/conf.d/php-xdebug.ini:ro
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro
    depends_on:
      - app
```

注意点として、普通にビルドスクリプトを記述していれば同じビルドを2回実行したとしても問題がないはずですが、2回実行したときに問題が出るようなスクリプトを書かないようにしてください。

CMD型Dockerコンテナは当然ながらDockerコンテナそのものですので、 `depends_on` 指定を行うことでDockerコンテナ同士の起動時の順番を制御することができます。この場合、システム本体のコンテナである `app` の起動が完了してから `build-app` コンテナは実行されるわけですね。

`volumes` を適切に設定することで、2つ以上のDockerコンテナで同じファイルを共有することが可能です。

### [2] docker-compose exec越しに実行する

Docker内でコマンドを実行するにはもう一つ方法があり、こちらのほうがむしろ一般的でしょう。

dockerコマンド、もしくは docker-compose コマンドでホストからDockerコンテナ内の任意のコマンドを実行できます。

例えば `app` コンテナ内の `composer install` を実行したい場合、このように記述できるでしょう。

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

ソースツリー上の構築作業について、詳細な作業は全てDocker内で実行し、その開始点のみ docker-compose等の代理人に実行させるルールを徹底すればWindows環境でもMac(Linux)環境でも同じように運用できることになります。


## まとめ

ここまで３通りの解決手段をご提示しました。

みなさんのシステム開発に役立てば嬉しいです。

なにか疑問、質問、感想等あれば気軽にコメントしてください。