Dockerで(ほぼ)同じ開発環境を複数同時に起動したい

## はじめに
現在参画しているプロジェクトは、Dockerでローカルの開発環境を構築しています。
そのプロジェクトでDBのバージョンアップを行うことになり、動作確認をしたいため、新旧両バージョンのDBでのローカル開発環境を立ち上げたい！という作業をした時の記録です。


具体的には、以下のcomposeファイルで定義されているような構成の、DBのバージョンを変えたものを二つUPしたいという内容です。

<details><summary>docker-compose.yml</summary>

```yml
version: '3.3'
services:
  proxy:
    build: ../proxy
    environment:
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.210.5
      REDIS_HOST: 192.168.210.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ../proxy:/app
    networks:
      app:
        ipv4_address: 192.168.210.8
  api:
    build: ../api
    environment:
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.210.5
      REDIS_HOST: 192.168.210.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.1:9292:9292"
    volumes:
      - ../api:/app
    networks:
      app:
        ipv4_address: 192.168.210.3
  web:
    build: ../web
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.210.5
      REDIS_HOST: 192.168.210.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - ../web:/app
    networks:
      app:
        ipv4_address: 192.168.210.6
  db:
    image: mysql:5.7.26
    environment:
      TZ: Asia/Tokyo
      MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
      MYSQL_USER: root
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports:
      - "127.0.0.1:3306:3306"
    volumes:
      - local-dev-mysql:/var/lib/mysql
    networks:
      app:
        ipv4_address: 192.168.210.5
  redis:
    image: redis:latest
    environment:
      TZ: Asia/Tokyo
    ports:
      - "127.0.0.1:6379:6379"
    networks:
      app:
        ipv4_address: 192.168.210.7
networks:
  app:
    ipam:
      config:
        - subnet: 192.168.210.0/24

volumes:
  local-dev-mysql:
    external: true
```
</details>


ここ(↓)を変えたものを2環境使いたい、というネタです。
```diff_yaml
  db:
-   image: mysql:5.7.26
+   image: mysql:8.0.28
```


## 参考にした記事
本文中に説明不足なところがあるかと思いますので、こちらの記事も参照いただくとよいと思います。
* [docker compose (docker-compose) で複数サービスを起動する](https://qiita.com/chimayu/items/dc52dd6e21295d326046)
* [【Docker】ローカルPCで同じポート番号をもったコンテナを複数立ち上げる](https://blog.framinal.life/entry/2021/08/17/023028)
* [dockerのコンテナを複数、同じポートを使って立ち上げたい](https://www.key-p.com/blog/staff/archives/107891)
* [Compose ファイル・リファレンス](https://docs.docker.jp/v19.03/compose/compose-file.html)



## 試した端末情報
**①macOS Ventura 13.6.7** (M2 Mac×colima) ※普段の開発環境
```shell
docker -v
Docker version 24.0.4, build 3713ee1eea

docker-compose -v
Docker Compose version 2.20.0

docker compose version
Docker Compose version 2.20.0
```

**②Windows 11 Pro 23H2** (Win11×WSL2×Ubuntu22)
```shell
docker -v
Docker version 26.1.3, build b72abbb

docker-compose -v
docker-compose version 1.29.2, build unknown

docker compose version
Docker Compose version v2.27.0
```

**③macOS Sonoma 14.5** (intel Mac×Docker Desktop)
```shell
docker -v
Docker version 27.0.3, build 7d4bcd8

docker-compose -v
zsh: command not found: docker-compose

docker compose version
Docker Compose version v2.28.1-desktop.1
```

**④Windows 10 Pro 22H2** (Win10×Docker Desktop)
```shell
docker -v
Docker version 27.0.3, build 7d4bcd8

docker-compose -v
Docker Compose version v2.28.1-desktop.1

docker compose version
Docker Compose version v2.28.1-desktop.1
```


## 想定するディレクトリ構成
```
PJ_root
│
┝ local-dev-mysql57
│　┝ proxy
│　│　└ Dockerfile
│　┝ api
│　│　└ Dockerfile
│　┝ web
│　│　└ Dockerfile
│　└ compose
│　　　└ docker-compose.yml
│
└ local-dev-mysql8
　　┝ proxy
　　│　└ Dockerfile
　　┝ api
　　│　└ Dockerfile
　　┝ web
　　│　└ Dockerfile
　　└ compose
　　　　└ docker-compose.yml
```

## やったこと
### 1. composeディレクトリの名前変更
デフォルトだと、カレントディレクトリ名がプロジェクト名として使用されるため([参考](https://docs.docker.jp/v19.03/compose/reference/overview.html))、ディレクトリ名を変更しました。
こうしておくと、`docker compose`コマンドを実行する時に`-p <プロジェクト名>`を指定しなくてもよくなるので楽ちんです。

`local-dev-mysql57\compose`→`local-dev-mysql57\mysql57`
`local-dev-mysql8\compose`→`local-dev-mysql8\mysql8`

プロジェクト名はnetwork名やimage名のprefixに使用されているので、後々`docker compose ps`を実行した時に表示される名前なども「mysql8-api」などのようになり、分かりやすくなります。



### 2. docker-compose.yml編集
重複しなければよいので、とりあえず片方のcomposeファイルだけ編集します。

#### 2-1. dbをマウントするvolume名の変更
データ保存先は分けないとダメなので、別のvolumeを指定するようにします。
(当然、それぞれのvolumeをcreateする必要あり)

```diff_yaml:local-dev-mysql8\mysql8\docker-compose.yaml
services:
  db:
    volumes:
-     - local-dev-mysql:/var/lib/mysql
+     - local-dev-mysql8:/var/lib/mysql

volumes:
- local-dev-mysql:
+ local-dev-mysql8:
    external: true
```
最終的に`volume`は以下のようになります。(上二つはそれぞれのアプリのvolume)
```
$ docker volume ls
DRIVER    VOLUME NAME
local     9cc6e617e5a98eed5cee86f18d5b6230259fec22ef9da32043d13594e481ff6e
local     dbaa795f95554b76ed0ba0153bdf0847108a980e3d90e0b5330f277f6aef4633
local     local-dev-mysql
local     local-dev-mysql8
```

#### 2-2. `networks`で指定している`subnet`の値変更
上記のディレクトリ構成で、local-dev-mysql57とlocal-dev-mysql8に対してそれぞれ`docker compose up`を実行すると、networkは別々に作成されます。
が、後からupした方は「subnetが既に使われている」というエラーになります。
なので、composeファイルで指定しているsubnetを変更します。

```diff_yaml:local-dev-mysql8\mysql8\docker-compose.yaml
networks:
  app:
    ipam:
      config:
-       - subnet: 192.168.210.0/24
+       - subnet: 192.168.211.0/24
```

#### 2-3. 各`service`に指定している`ipv4_address`の値変更
上記で`subnet`を変更するので、各`service`で指定しているIPアドレスも変更します。
```diff_yaml:local-dev-mysql8\mysql8\docker-compose.yaml
services:
  proxy:
    networks:
      app:
-       ipv4_address: 192.168.210.8
+       ipv4_address: 192.168.211.8
```

2-2, 2-3については`docker-compose.yaml`内で一括置換すると間違えなくてよいです。
vimコマンド `:%s/192.168.210/192.168.211/gc`


#### 2-4. 各`service`に指定している`ports`の値変更
ここが一番厄介だったのですが、、結論から言うとDocker Desktopとそれ以外で振る舞いが違いました。

Docker Desktop(Mac,Win共)を使用した場合、以下のように記載したら「http&#58;//127.0.0.2:3000」でブラウザからアクセスできました。

:::note warn
Macの場合は、「127.0.0.2」をループバックアドレスに追加する必要があります。詳細は[参考にした記事](#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E8%A8%98%E4%BA%8B)をご確認ください。
:::

```diff_yaml:local-dev-mysql8\mysql8\docker-compose.yaml
services:
  web:
    ports:
-     - "127.0.0.1:3000:3000"
+     - "127.0.0.2:3000:3000"
```
当然、composeファイルを変更していない方の環境は「http&#58;//127.0.0.1:3000」でアクセスできます。

　
Win×WSL2×Ubuntu22環境の場合、`docker compose ps`で確認すると問題なく起動しているのですが、「http&#58;//127.0.0.2:3000」でブラウザからアクセスできませんでした。
WSLに接続したコンソールから`curl`コマンドを実行したところ、応答は返ってくるので、どちらかというと「Win×WSL2」の問題な気がしています。　(が、解決はしていない。。)
```
$ curl -v http://127.0.0.2:3000
*   Trying 127.0.0.2:3000...
* Connected to 127.0.0.2 (127.0.0.2) port 3000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.2:3000
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< X-Frame-Options: SAMEORIGIN
< X-Xss-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Location: http://127.0.0.2:3000/sessions/new
< Content-Type: text/html; charset=utf-8
< Cache-Control: no-cache
< X-Request-Id: 72837b30-b40c-4f03-a32f-9d23503f947d
< X-Runtime: 0.994378
< Server: WEBrick/1.4.2 (Ruby/2.6.3/2019-04-16)
< Date: Fri, 09 Aug 2024 00:53:10 GMT
< Content-Length: 100
< Connection: Keep-Alive
<
* Connection #0 to host 127.0.0.2 left intact
<html><body>You are being <a href="http://127.0.0.2:3000/sessions/new">redirected</a>.</body></html>
```

　
Mac×colima環境も`docker compose ps`で確認すると問題なく起動しているのですが、ブラウザからのアクセスはできず、`curl`コマンドもエラーになりました。
※`127.0.0.2`はループバックアドレスに追加済です。
```
% docker compose ps
NAME           IMAGE        COMMAND                  SERVICE   CREATED       STATUS       PORTS
mysql8-web-1   mysql8-web   "bash -c 'rm -f /app…"   web       2 weeks ago   Up 2 weeks   127.0.0.2:3000->3000/tcp

% curl -v http://127.0.0.2:3000
*   Trying 127.0.0.2:3000...
* connect to 127.0.0.2 port 3000 failed: Connection refused
* Failed to connect to 127.0.0.2 port 3000 after 0 ms: Couldn't connect to server
* Closing connection
curl: (7) Failed to connect to 127.0.0.2 port 3000 after 0 ms: Couldn't connect to server
```

　
というわけで、Docker Desktopではない環境の場合はホスト側のポート番号を変えるという地味に面倒な作業で対応しました。
```diff_yaml:local-dev-mysql8\mysql8\docker-compose.yaml
services:
  web:
    ports:
-     - "127.0.0.1:3000:3000"
+     - "127.0.0.1:13000:3000"
```

　
[Compose ファイル・リファレンス](https://docs.docker.jp/v19.03/compose/compose-file.html#ports)を見ると、`ports`は「ホスト側:コンテナ側」のポート割り当てとのことなので、WSLが挟まるとWindowsのブラウザからうまくアクセスできない、というのは何となく分かるのですが、Mac×colimaの時の動きがよく分からないです。
逆に言うと、Docker Desktopはどういう処理をしているのか？という疑問でもあるわけですが。。


## さいごに
これをやるまではdocker-compose.ymlに書いてある内容はほとんど理解しておらず、機械的に`docker compose stop`と`docker compose up -d`で環境を切り替えていました。しかし、面倒くさい！となり、いろいろと調べた結果やりたかったことが実現できた上にcomposeファイルの中身の理解も進んで、やってみてよかったなぁと思いました。謎は残ってはいますが。。

　
変更したcomposeファイルを置いておきます。
誰かの役に立ったらうれしいです。

<details><summary>docker-compose.yml 修正案1</summary>

```yml
version: '3.3'
services:
  proxy:
    build: ../proxy
    environment:
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.211.5
      REDIS_HOST: 192.168.211.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.2:8080:8080"
    volumes:
      - ../proxy:/app
    networks:
      app:
        ipv4_address: 192.168.211.8
  api:
    build: ../api
    environment:
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.211.5
      REDIS_HOST: 192.168.211.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.2:9292:9292"
    volumes:
      - ../api:/app
    networks:
      app:
        ipv4_address: 192.168.211.3
  web:
    build: ../web
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.211.5
      REDIS_HOST: 192.168.211.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.2:3000:3000"
    volumes:
      - ../web:/app
    networks:
      app:
        ipv4_address: 192.168.211.6
  db:
    image: mysql:8.0.28
    environment:
      TZ: Asia/Tokyo
      MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
      MYSQL_USER: root
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports:
      - "127.0.0.2:3306:3306"
    volumes:
      - local-dev-mysql8:/var/lib/mysql
    networks:
      app:
        ipv4_address: 192.168.211.5
  redis:
    image: redis:latest
    environment:
      TZ: Asia/Tokyo
    ports:
      - "127.0.0.2:6379:6379"
    networks:
      app:
        ipv4_address: 192.168.211.7
networks:
  app:
    ipam:
      config:
        - subnet: 192.168.211.0/24

volumes:
  local-dev-mysql8:
    external: true
```
</details>

<details><summary>docker-compose.yml 修正案2</summary>

```yml
version: '3.3'
services:
  proxy:
    build: ../proxy
    environment:
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.211.5
      REDIS_HOST: 192.168.211.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.1:18080:8080"
    volumes:
      - ../proxy:/app
    networks:
      app:
        ipv4_address: 192.168.211.8
  api:
    build: ../api
    environment:
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.211.5
      REDIS_HOST: 192.168.211.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.1:19292:9292"
    volumes:
      - ../api:/app
    networks:
      app:
        ipv4_address: 192.168.211.3
  web:
    build: ../web
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
      DOCKER: 'true'
      DB_HOST: 192.168.211.5
      REDIS_HOST: 192.168.211.7
      REDIS_PORT: 6379
    tty: true
    stdin_open: true
    ports:
      - "127.0.0.1:13000:3000"
    volumes:
      - ../web:/app
    networks:
      app:
        ipv4_address: 192.168.211.6
  db:
    image: mysql:8.0.28
    environment:
      TZ: Asia/Tokyo
      MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
      MYSQL_USER: root
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports:
      - "127.0.0.1:13306:3306"
    volumes:
      - local-dev-mysql8:/var/lib/mysql
    networks:
      app:
        ipv4_address: 192.168.211.5
  redis:
    image: redis:latest
    environment:
      TZ: Asia/Tokyo
    ports:
      - "127.0.0.1:16379:6379"
    networks:
      app:
        ipv4_address: 192.168.211.7
networks:
  app:
    ipam:
      config:
        - subnet: 192.168.211.0/24

volumes:
  local-dev-mysql8:
    external: true
```
</details>