dockerのportアクセスをより良くしたい



こんにちは。Legalscapeソフトウェアエンジニアの小林です。
この記事は[Legalscape アドベントカレンダー 2021](https://qiita.com/advent-calendar/2021/legalscape)の12/10のエントリです。

皆さん、Docker使ってますか？便利ですよね（テンプレ）
そんな便利なDockerですが、個人的にイマイチポイントだなと思っているのがport 周りの扱いです。
例えば、Mysqlのcontainerを立てた時に3306ポートをhostの3306ポートにmappingするのが一般的かと思います。

```yaml
version: "3.3"

services:
  mysql:
    image: mysql:8.0
    ports:
      - 3306:3306
    volumes:
      - ./.data/mysql:/var/lib/mysql
    environment:
      - TZ=Asia/Tokyo
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
```

こうすることで、hostの3306を通じてMysqlに接続できますね。便利です。[^1]

[^1]: 余談ですが、docker-composeはコンテナ起動の設定ファイルと認識しているので、1コンテナしか立てない場合も積極的に利用しています。

...

いや、本当に便利ですか？
Dockerが普及した今となっては、複数projectで別のdocker-composeを利用することが多いと思います。その時、各projectにMysqlコンテナが存在したらどうでしょうか？もちろん `Bind for 0.0.0.0:3306 failed: port is already allocated.` となりますね。
よく使う回避策としてはhost側portを別のportにmappingすることでしょうか。[^2]

[^2]: Mysql固有の回避策として、host側のclientを使わず、docker execでcontaier側のclientを使う方法（これであればそもそもport mappingが不要）が有りますが、portを使用するアプリケーション全般に使用できる方法ではないので、今回はこれについては考えません。

``` yaml
# 前
    ports:
      - 13306:3306
# 略
```

この13306のところをprojectごとに変えれば `Bind for 0.0.0.0:3306 failed: port is already allocated.` は発生しなくなりますね。めでたしめでたし！

...

本当に？
個人的には、この手法は以下の点でイマイチだと考えています。

1. 各projectでport番号を覚えてないといけない

     「このprojectのMysqlのportは何番だっけ？」
1. 他のprojectで使用したport番号を回避しないといけない

    「とりあえず53306とかにmappingするか。・・・被ってるやんけ！」

この問題を回避するためにはどうすればよいでしょうか？

## 解決策1: （若干イケてない）

先にイケてないポイントを書きますと、GUIユーザーが泣きを見ます。僕は基本的にCUIを使う人間なのでこの方法で8割程度満足できます。
具体的な方法ですが、mappingに使用するhost側のportをランダムにします。

``` yaml
# 前
    ports:
      - 3306
# 略
```

port publish時にhost側のportを指定しなければ、hostのランダムなportにmappingしてくれます。
どのportにmappingされたかは、`docker ps` や `docker inspect` 、 `docker compose port` を使用すればわかります。

``` :docker&#x20;ps
CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                                    NAMES
353e24477126   mysql:8.0   "docker-entrypoint.s…"   10 minutes ago   Up 10 minutes   33060/tcp, 0.0.0.0:49583->3306/tcp, :::49583->3306/tcp   clever_jemison
```

``` json:docker&#x20;inspect
[
    {   
        "NetworkSettings": {
            "Ports": {
                "3306/tcp": [
                    {   
                        "HostIp": "0.0.0.0",
                        "HostPort": "49583"
                    },
                    {   
                        "HostIp": "::",
                        "HostPort": "49583"
                    }   
                ]
                "33060/tcp": null
            },
            ...
        },
        ...
    }   
]
```

``` text:docker-compose&#x20;port&#x20;mysql&#x20;3306
0.0.0.0:49583
```

Makefileでこのportを使ってMysql cliを実行するtargetを定義します。

```makefile:Makefile
mysql-cli:
	mysql --port $(shell docker-compose port mysql 3306 | cut -d: -f2) --host 127.0.0.1 --user root -p
```

上述したとおり、この方法ではコマンドを駆使してランダムに振り当てられたportを取得しているので、GUIを用いてMysqlに繋いでいる人はとても困ります。

## 解決策2: （問題点をほぼ完全に解決できるが、現時点でlinuxでしか動かない）

そもそもやりたいことは、containerのportにアクセスしたいだけなので、それをport mapping無しで実現できれば解決します。
そう、contianerのprivateIPを使用すればよいのです。
docker inspectを使えば、containerのprivate IPがわかります。

``` json:docker&#x20;inspect
[
    {   
        "NetworkSettings": {
            "Networks": {
                "app_default": {
                    "IPAddress": "172.24.3.2",
                    ...
                }
            }
            ...
        },
        ...
    }   
]

```shell
mysql -h 172.24.3.2 --port 3306 -u root -p
```

これであればportが重複するなどという問題はそもそも起こりません。IPを調べる手間を除けば完璧に思えます。
ただ、IPを調べるのが面倒くさいです。面倒臭すぎます。
というわけでこれを解決するアプリケーションを作りました。

[docker-hosts](https://github.com/skobaken7/docker-hosts)

Dockerコンテナとして起動しておくと、他のコンテナが起動する度にhostの `/etc/hosts` を書き変えて、コンテナにアクセスするためのドメインを用意してくれます。
例えば、

```yaml
version: "3.3"

services:
  mysql:
    image: mysql:8.0
    ports:
      - 3306
    volumes:
      - ./.data/mysql:/var/lib/mysql
    environment:
      - TZ=Asia/Tokyo
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

networks:
  default:
    name: projectX
```

このようなcomposeファイルでMysqlコンテナを起動すると `mysql.projectX.docker.internal` というドメインを割り当ててくれるので、

```shell
mysql -h mysql.projectX.docker.internal --port 3306 -u root -p
```

という感じでMysqlに接続できます。どのprojectのMysqlに繋いでいるかも分かりやすいですね。完璧です。

唯一の問題点はMacで動作しないことです（windowsでの動作可否は未確認） 。`/etc/private/hosts` を書き換える時に権限エラーが出てしまいます。
バイナリをDockerコンテナとしてではなくネイティブバイナリとしてsudo付きで実行すればこの問題は解決すると思われますが、PC起動時に `docker-hosts` の起動が必要になってしまい若干手間なので、なんとかDockerコンテナとして実行できないか模索中です。
解決策をご存じの方がいらっしゃいましたら、是非ご教授ください。



（これに似たようなことDocker本体でやってくれないかなー）

## 終わりに

Legalscapeではエンジニアの採用を積極的に行っています。是非 [採用ページ](https://www.notion.so/legalscape/09aeb478072946c18249495b8fb63fcd) も見ていってください！
Legalscapeの事業内容や技術スタックなどは、アドベントカレンダーの他エントリをご覧ください。