まだdocker-composeのホスト側portを考えるのに疲弊しているの？ 〜IP指定してwell-known ports使い放題、同時に1677万案件回す〜



タイトルは釣りです

1677万案件は試してません

# 対象読者 #

- 開発環境を docker-compose で構築しているWebプログラマの人々
- 複数案件を抱え、ホスト側ポート番号を考えるのに疲弊している人々
  - HTTP `80` -> `80`はとっておいて`8080`にしたろ！
  - あの案件で`8080`を使ったからこっちは`10080`にしたろ！
  - `10080`の次だからとりあえず`20080`にしたろ！
  - うーん、`8000`！ｗ
  - HMRで`8080`使いたいけれど使われてるから`8081`にしたろ！
  

# 問題提起 #

docker-compose.ymlの例




```yaml:anken-1/docker-compose.yml
version: "3"
services:
  web:
    image: "nginx"
    ports:
      - "80:80" # ...... 1
    ...
  app:
    image: "php-fpm"
    ...
  db:
    image: "mysql:5.7"
    ports:
      - "3306:3306" # ...... 2
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
  secure-db:
    image: "mysql:5.7"
    ports:
      - "3307:3306" # ...... 3
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
```

- ホストOSのブラウザで動作確認するためにHTTPサーバーのポートを公開する(`80`)
- 同じくホストから繋ぐためにDBのポートを公開する(`3306`)
- DBは複数立っていたりする(`3307`)



さて別の案件が降ってきました。



```yaml:anken-2/docker-compose.yml
version: "3"
services:
  app:
    image: "httpd:2.4"
    ports:
      - "8080:80" # ...... 1
    volumes:
      - "./public/:/usr/local/apache2/htdocs/"
  db:
    image: "mysql:5.7"
    ports:
      - "13306:3306" # ...... 2
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
  secure-db:
    image: "mysql:5.7"
    ports:
      - "13307:3306" # ...... 3
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
```

先の案件で振ったポート番号とぶつからないように、**well-knownポートの亜種**をいい感じに考えて割り当てます

- `80`は別の案件で使ったので`8080`
- `3306`は別の案件で使ったので`13306`
- `3307`は別の案件で使ったので`13307`


さて最初の案件でHMRを導入したくなりました。



```diff:anken-1/docker-compose.yml
  version: "3"
  services:
    web:
      image: "nginx"
      ports:
        - "80:80"
      ...
    app:
      image: "php-fpm"
      ...
    db:
      image: "mysql:5.7"
      ports:
        - "3306:3306"
      environment:
        - "MYSQL_ROOT_PASSWORD=root"
    secure-db:
      image: "mysql:5.7"
      ports:
        - "3307:3306"
      environment:
        - "MYSQL_ROOT_PASSWORD=root"
+   node:
+     build:
+       ...
+     ports:
+       - "8080:8080" # ...... 4
```

HMR用のNode.jsサーバーを立てたくなりました。
んじゃ8080で…




```yaml:anken-2/docker-compose.yml
...
  app:
    image: "httpd:2.4"
    ports:
      - "8080:80"
...
```

あ！8080を既に使っていました…



```diff:anken-1/docker-compose.yml
  version: "3"
  services:
    web:
      image: "nginx"
      ports:
        - "80:80"
      ...
    app:
      image: "php-fpm"
      ...
    db:
      image: "mysql:5.7"
      ports:
        - "3306:3306"
      environment:
        - "MYSQL_ROOT_PASSWORD=root"
    secure-db:
      image: "mysql:5.7"
      ports:
        - "3307:3306"
      environment:
        - "MYSQL_ROOT_PASSWORD=root"
+   node:
+     build:
+       ...
+     ports:
+       - "8081:8080" # ...... 4
```

8081にしておこう…と、こうなります。

別々の案件を抱える複数の人間が絡むと混乱は加速します。

そのうちポート番号込みでgit管理するのが嫌になってきて、

「ホスト側portはもう各自の.envで管理してくれ」

となります。






```yaml:anken-1/docker-compose.yml
version: "3"
services:
  web:
    image: "nginx"
    ports:
      - "${WEB_HOST_PORT}:80"
    ...
  app:
    image: "php-fpm"
    ...
  db:
    image: "mysql:5.7"
    ports:
      - "${DB_HOST_PORT}:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
  secure-db:
    image: "mysql:5.7"
    ports:
      - "${SECURE_DB_HOST_PORT}:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
  node:
    build:
      ...
    ports:
      - "${NODE_HMR_HOST_PORT}:8080"
```



```:anken-1/env_example
WEB_HOST_PORT=80
DB_HOST_PORT=3306
SECURE_DB_HOST_PORT=3307
NODE_HMR_HOST_PORT=8080
```




```yaml:anken-2/docker-compose.yml
version: "3"
services:
  app:
    image: "httpd:2.4"
    ports:
      - "${APP_HOST_PORT}:80"
    volumes:
      - "./public/:/usr/local/apache2/htdocs/"
  db:
    image: "mysql:5.7"
    ports:
      - "${DB_HOST_PORT}:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
  secure-db:
    image: "mysql:5.7"
    ports:
      - "${SECURE_DB_HOST_PORT}:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
```



```:anken-2/env_example
APP_HOST_PORT=80
DB_HOST_PORT=3306
SECURE_DB_HOST_PORT=3307
```


幾分マシになった感じがしますが、しばらくすると

- 「何箇所設定するね〜んｗｗ」
- 「何もしてないのに `docker-compose up -d` が立ち上がらなくなった」
  - env_exampleの更新を.envへ反映し忘れている


等々の問題が生じてきます。




# 解決案 -- ポートをバインドするIPを指定する #

- [サンプルrepository](https://github.com/wand2016/docker-composer-multiple-projects-sample)
  - 名前をdocker-compose **r** にしてしまう痛恨のミス



[公式doc](http://docs.docker.jp/engine/userguide/networking/default_network/binding.html)に書いてありますが、`docker run -p`ではホスト側ポートをバインドするIPアドレスを指定できます。


> １つは docker run の実行時、 -p IP:ホスト側ポート:コンテナ側ポート か 
> -p IP::ポート を指定し、特定の外部インターフェースをバインドする指定ができます。

「IPアドレス」というのは、は論理的なアドレスです。
物理的なホストに対して複数の論理的なアドレスが割り当て可能です。

往来で

> 「D.Horiyamaさんの80番ポートさん、HTMLファイルをください」

と言われたらHTMLファイルを返しますが、

> 「‡漆黒の堕天使‡さんの80番ポートさん、HTMLファイルをください」

と言われたら、こっ恥ずかしくて聞いてないふりしますよね。そういうことです。

「この名前で呼ばれた時だけ応答する」ということができるわけです。

さて、このホスト側IP指定、docker-compose.yml でも設定可能です: 



```yaml:docker-compose.yml
version: "3"
services:
  web:
    image: "httpd:2.4"
    ports:
      - "${IP}:80:80"
    volumes:
      - "./public/:/usr/local/apache2/htdocs/"
  db:
    image: "mysql:5.7"
    ports:
      - "${IP}:3306:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
  secure-db:
    image: "mysql:5.7"
    ports:
      - "${IP}:3307:3306"
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
```



```:.env
IP=127.0.0.1
```

こんな風にします。


`127.*.*.*`は **ローカルループバックアドレス** と呼ばれる特別なアドレスで、ホスト自身を指します。
`127.0.0.1`〜`127.255.255.254` は全て別のIPアドレスであり、個々にポートをバインドできます。
これを使って、

**「IP=127.0.0.1は案件1用」**

というようにして、**`80`や`3306`といったwell-known portを惜しげもなくじゃぶじゃぶ使うことができます。**

```
$ docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                 NAMES
36d5b663edf6        mysql:5.7           "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds        127.0.0.2:3306->3306/tcp, 33060/tcp   anken-2_db_1
c347fe183ad6        httpd:2.4           "httpd-foreground"       3 seconds ago       Up 2 seconds        127.0.0.2:80->80/tcp                  anken-2_web_1
8ad0a5ae018d        mysql:5.7           "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds        33060/tcp, 127.0.0.2:3307->3306/tcp   anken-2_secure-db_1
6955a89c43d9        httpd:2.4           "httpd-foreground"       29 seconds ago      Up 28 seconds       127.0.0.1:80->80/tcp                  anken-1_web_1
f13161e0dfef        mysql:5.7           "docker-entrypoint.s…"   29 seconds ago      Up 28 seconds       33060/tcp, 127.0.0.1:3307->3306/tcp   anken-1_secure-db_1
c1905356e589        mysql:5.7           "docker-entrypoint.s…"   29 seconds ago      Up 28 seconds       127.0.0.1:3306->3306/tcp, 33060/tcp   anken-1_db_1
```

↑`80`,`3306`,`3307`をanken-1, anken-2それぞれで利用できています。

`127.0.0.1:80`, `127.0.0.2:80` にリクエストしてみると:

```
$ curl 127.0.0.1:80
<html>案件1</html>

$ curl 127.0.0.2:80
<html>案件2</html>
```

ちゃんと、それぞれ別々のサーバーに処理されていることがわかります。

---

さて、IPv4発明当初の仕様策定陣は、これほどまでにインターネットが隆盛を極めるとは考えていなかったのでしょう。

**全アドレス空間のうち1/256 -- 約1677万個 -- もの膨大なアドレス空間をローカルループバックアドレスに宛ててしまいました。**
(グローバルIPアドレスとして使えなくなってしまいました)


おかげさまで、この方法で、**理論的には約1677万案件ぶんの docker-compose 環境を同時に抱えられます！**(タイトル回収)




生のIPアドレスはつらいので、`/etc/hosts`を書いたりDNSを立てたりするとより便利になるでしょう:




```:/etc/hosts
...
127.0.0.1 anken-1
127.0.0.2 anken-2
...
```

curl

```
$ curl anken-1:80
<html>案件1</html>
```

mysql

```
$ mysql -h anken-1 -u root -proot

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.27 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> \q
```


# nginx-proxyとの違い #

[nginx-proxy](https://hub.docker.com/r/jwilder/nginx-proxy/)

> ？「nginx-proxyでいいんじゃ？」


nginx-proxyはHTTPにしか使えないと聞きます。(使ったことない)

対して、本記事の方法はHTTP以外にも適用可能です。


# macOS固有の問題 #

## Docker Desktop for mac 2.2.x の不具合 ##

[不具合スレ](https://github.com/docker/for-mac/issues/4209)


Docker Desktop for macでは、2.2.x系にて不具合があり、127.0.0.1(デフォルト)以外のIPアドレスのポートをバインドできなかったようです。

さっさと2.3系に上げましょう。


## 127.0.0.1 以外のローカルループバックアドレスがデフォルトで使えない ##

[困ってる人々](https://superuser.com/questions/458875/how-do-you-get-loopback-addresses-other-than-127-0-0-1-to-work-on-os-x)


デフォルトで`ping 127.0.0.2`すら通りません。`ifconfig`でエイリアス定義する必要があります。

下記のようなスクリプトを `.zshrc`などで実行するとよいでしょう:


```sh
for ((i=2;i<256;i++))
do
    sudo ifconfig lo0 alias 127.0.0.$i up
done
```