docker-compose の ports 指定まとめ

# 前提
## 各種バージョン
```sh
$ docker.exe -v
Docker version 18.03.0-ce, build 0520e24302
```
```sh
$ docker-compose.exe -v
docker-compose version 1.20.1, build 5d8c71b2
```
```sh
$ docker-machine.exe ssh default 'docker -v'
Docker version 18.09.1, build 4c52b90
```

```yaml:docker-compose.yml
version: '3'
```

## ホストのIP
- `192.168.99.100`
- `10.0.2.15`
- 他
    - `172` 始まりのIPは Docker が作成するので一旦無視する

```sh
docker@default:~$ ifconfig  | grep "inet addr"
          inet addr:172.19.0.1  Bcast:172.19.255.255  Mask:255.255.0.0
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
          inet addr:192.168.99.100  Bcast:192.168.99.255  Mask:255.255.255.0
          inet addr:127.0.0.1  Mask:255.0.0.0
```

## 対象プロトコル
- IP v4
- TCP
    - Dockerのportsでは TCPとUDP の両方を指定できるが、今回は TCP のみを対象とする。

## 検証方法
[nginx](https://nginx.org/) を使用して コンテナ内にHTTP(80番port)サーバーを動かす。

```yaml:docker-compose.yml
version: '3'

services:
  nginx:
    image: nginx:1.15
    ports:
      - "127.0.0.1:8080:80" # ここを変更していく
```

# 検証結果
## 1対1のport mapping
各ports指定の :o:, :x: は各URLへ `curl` でアクセスできるかの結果

### `"<Host IP>:<Host Port>:<Container Port>"`

[^127.0.0.0/8]: 127始まり（第一オクテットが`127`）のIP（例: `127.1.2.3`）も `127.0.0.1` にように使える。port番号で区別しなくて良いので便利。

#### `"127.0.0.1:8080:80"`
- :o: http://127.0.0.1:8080
    - :o: http://localhost:8080
- :x: http://127.1.2.3:8080 [^127.0.0.0/8]
- :x: http://192.168.99.100:8080
- :x: http://10.0.2.15:8080

```sh:host側のport使用状況
docker@default:~$ netstat -ant | grep -E "8080|State"
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN
```

#### `"127.1.2.3:8080:80"` [^127.0.0.0/8]
- :x: http://127.0.0.1:8080
- :o: http://127.1.2.3:8080 [^127.0.0.0/8]
- :x: http://192.168.99.100:8080
- :x: http://10.0.2.15:8080

```sh:host側のport使用状況
docker@default:~$ netstat -ant | grep -E "8080|State"
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.1.2.3:8080          0.0.0.0:*               LISTEN
```

#### `"192.168.99.100:8080:80"`
ループバック `127.0.0.0/8` を指定した場合と違い、 `192.168.99.0/24` ネットワークの他のホストからのアクセスを受け付けられるようになる。
※ Firewallなどで外からの通信が許可されている場合に限る[^docker-firewall]

[^docker-firewall]: Windows(Docker Desktop for Windows) の場合、Windowsのファイアウォール設定上では `vpnkit` を許可すると別ホストからのアクセスが可能となる。Linuxの場合、Dockerはiptablesの設定を書き換えるため意図しない公開に注意（参考: [Dockerでマストドンやってたら、遮断したはずのポートが丸見えだった件 | Divide et impera](https://glorificatio.org/archives/2953)）

- :x: http://127.0.0.1:8080
- :x: http://127.1.2.3:8080 [^127.0.0.0/8]
- :o: http://192.168.99.100:8080
- :x: http://10.0.2.15:8080

```sh:host側のport使用状況
docker@default:~$ netstat -ant | grep -E "8080|State"
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 192.168.99.100:8080     0.0.0.0:*               LISTEN
```

#### `"0.0.0.0:8080:80"`
自ホスト宛の通信ならネットワーク・IPアドレスを問わず受け付けてコンテナに転送する

- :o: http://127.0.0.1:8080
- :o: http://127.1.2.3:8080 [^127.0.0.0/8]
- :o: http://192.168.99.100:8080
- :o: http://10.0.2.15:8080

```sh:host側のport使用状況
docker@default:~$ netstat -ant | grep -E "8080|State"
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 :::8080                 :::*                    LISTEN
```

#### :x: `"8.8.8.8:8080:80"` [^8.8.8.8]
ホストが持っていないIPを指定するとエラーとなりコンテナを作成できない

```
$ docker-compose.exe up -d
Removing dockernginx_nginx_1
Recreating 0ad48208ae60_dockernginx_nginx_1 ... error

ERROR: for 0ad48208ae60_dockernginx_nginx_1  Cannot start service nginx: driver failed programming external connectivity on endpoint dockernginx_nginx_1 (ed3d7ae26be69b038d3896e637a0163449fa524a93ddfe98a5673190e7519e06): Error starting userland proxy: listen tcp 8.8.8.8:8080: bind: cannot assign requested address

ERROR: for nginx  Cannot start service nginx: driver failed programming external connectivity on endpoint dockernginx_nginx_1 (ed3d7ae26be69b038d3896e637a0163449fa524a93ddfe98a5673190e7519e06): Error starting userland proxy: listen tcp 8.8.8.8:8080: bind: cannot assign requested address
Encountered errors while bringing up the project.
```

[^8.8.8.8]: `8.8.8.8` はGoogleの所有するIPアドレス。[Google Public DNS](https://developers.google.com/speed/public-dns/) として公開されている。


### `"<Host Port>:<Container Port>"` (`<Host IP>:`を省略した場合)
#### `"8080:80"`
- `<Host IP>` を `0.0.0.0` とした場合(`"0.0.0.0:8080:80"`) と同じ
    - :o: http://127.0.0.1:8080
    - :o: http://127.1.2.3:8080
    - :o: http://192.168.99.100:8080
    - :o: http://10.0.2.15:8080

```sh:host側のport使用状況
docker@default:~$ netstat -ant | grep -E "8080|State"
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 :::8080                 :::*                    LISTEN
```

```sh:docker-composeも0.0.0.0として認識している
$ docker-compose port nginx 80
0.0.0.0:8080
```

### `"<Container Port>"` (`<Host IP>:<Host Port>:`を省略した場合)
#### `"80"`
- `<Host IP>`: `0.0.0.0` を指定したとみなされる（`"<Host Port>:<Container Port>"` 指定のときと同じ）
- `<Host Port>`: 使用可能なポート番号（[エフェメラルポート](https://www.wdic.org/w/WDIC/%E3%82%A8%E3%83%95%E3%82%A7%E3%83%A1%E3%83%A9%E3%83%AB%E3%83%9D%E3%83%BC%E3%83%88)）をDockerが選択
    - コンテナを作り直した際に同一Port番号となることは保証されていない


```sh:コンテナ作成の都度port番号は変わる
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:32777
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:32778
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:32779
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:32780
```

## 範囲（Range）指定
###  `"8080-8082:80-82"`
範囲内のIPの数が同じなら `<Host Port>` と `<Container Port>` が1対1対応する

- `0.0.0.0:8080->80`
- `0.0.0.0:8081->81`
- `0.0.0.0:8082->82`

```sh
$ docker-compose ps
       Name                 Command          State                                Ports
---------------------------------------------------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:8080->80/tcp, 0.0.0.0:8081->81/tcp, 0.0.0.0:8082->82/tcp
```

###  `"8080-8082:80"` （`<Host Port>` 側だけ範囲指定）
`<Host Port>` 側の範囲（`8080`, `8081`, `8082`）から使用可能なportをdockerが選択する。
`<Host IP>:<Host Port>:` を省略した `"<Container Port>"` 指定の動きに近い[^Container_Port指定の動きに近い]。

[^Container_Port指定の動きに近い]: `"80"` 指定は（Boot2Docker の TinyCoreLinuxの場合） `"32768-60999:80"` 指定と同じだと考えらられる

```sh
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Creating network "dockernginx_default" with the default driver
Creating dockernginx_nginx_1 ... done
0.0.0.0:8081
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:8082
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:8080
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:8081
$ docker-compose up -d --force-recreate; docker-compose port nginx 80
Recreating dockernginx_nginx_1 ... done
0.0.0.0:8082
```

#### いつ使うか？（`<Host Port>` 側だけ範囲指定できることの嬉しさ）
`--scale` オプションを指定して、（1種類のServiceで）2つ以上のコンテナを作成するとき。

portを1対1（例: `"8080:80"`）で指定しているときに `--scale` オプションを使用して2つ以上のコンテナを作成するとport重複によって1つしかコンテナを作成できない


```sh:1対1指定したときはscaleオプションで2つ以上のコンテナを作成できない
$ docker-compose up -d --scale nginx=2
Creating network "dockernginx_default" with the default driver
The "nginx" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Creating dockernginx_nginx_1 ... error
Creating dockernginx_nginx_2 ... done

ERROR: for dockernginx_nginx_1  Cannot start service nginx: driver failed programming external connectivity on endpoint dockernginx_nginx_1 (8f55e9f086d4beea21daf076a731d998f6ec8440df4b28c99fb4f1a97272164d): Bind for 0.0.0.0:8080 failed: port is already allocated

ERROR: for nginx  Cannot start service nginx: driver failed programming external connectivity on endpoint dockernginx_nginx_1 (8f55e9f086d4beea21daf076a731d998f6ec8440df4b28c99fb4f1a97272164d): Bind for 0.0.0.0:8080 failed: port is already allocated
Encountered errors while bringing up the project.
$ docker-compose ps
       Name                 Command           State            Ports
----------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Exit 128
dockernginx_nginx_2   nginx -g daemon off;   Up         0.0.0.0:8080->80/tcp
```

`<Host Port>` 側を範囲指定（例: `"8080-8082:80"`）しておけば `--scale` オプションを使用して2つ以上のコンテナを作成してもport重複が発生せずコンテナの作成が成功する。

```sh:portを範囲指定していればscaleオプションで2つ以上のコンテナを作成できる
$ docker-compose up -d --scale nginx=3
Creating network "dockernginx_default" with the default driver
The "nginx" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Creating dockernginx_nginx_1 ... done
Creating dockernginx_nginx_2 ... done
Creating dockernginx_nginx_3 ... done
$ docker-compose ps
       Name                 Command          State          Ports
-------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:8081->80/tcp
dockernginx_nginx_2   nginx -g daemon off;   Up      0.0.0.0:8082->80/tcp
dockernginx_nginx_3   nginx -g daemon off;   Up      0.0.0.0:8080->80/tcp
```
※ コンテナの番号とport番号の順序は一致するとは限らないらしい

- 1 -> 8081
- 2 -> 8082
- 3 -> 8080

ちなみに、範囲の長さより少ないscale数を指定することも可能[^範囲の長さより多いscale数を指定]。
[^範囲の長さより多いscale数を指定]: 逆に、port範囲の長さより多いscale数を指定したら、port重複でコンテナは（一部）作成されない
この場合、使われないport番号が生じる（どれが使われないかは不定）。

```sh
$ docker-compose up -d --scale nginx=2
Creating network "dockernginx_default" with the default driver
The "nginx" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Creating dockernginx_nginx_1 ... done
Creating dockernginx_nginx_2 ... done
$ docker-compose ps
       Name                 Command          State          Ports
-------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:8082->80/tcp
dockernginx_nginx_2   nginx -g daemon off;   Up      0.0.0.0:8081->80/tcp
```

###  :x: `"8080:80-82"`
コンテナ側の範囲の長さの方が多い指定は許されない

```sh
$ docker-compose up -d
The Compose file '.\docker-compose.yml' is invalid because:
services.nginx.ports is invalid: Port ranges don't match in length
```
###  :x: `"8080-8082:80-89"`
同上。

```sh
$ docker-compose up -d
The Compose file '.\docker-compose.yml' is invalid because:
services.nginx.ports is invalid: Port ranges don't match in length
```

###  :x: `"8080-8089:80-82"`
`<Host Port>` と `<Container Port>` の範囲の長さが異なる場合も許されない

```sh
$ docker-compose up -d
The Compose file '.\docker-compose.yml' is invalid because:
services.nginx.ports is invalid: Port ranges don't match in length
```


###  `"80-82"`
["\<Container Port>" (\<Host IP>:\<Host Port>:を省略した場合)](#container-port-host-iphost-portを省略した場合) と同じく、コンテナ側の `80`, `81`, `82` をホスト側の空きportに適当に割り当てる

```sh
$ docker-compose up -d --force-recreate; docker-compose ps
Creating network "dockernginx_default" with the default driver
Creating dockernginx_nginx_1 ... done
       Name                 Command          State                                  Ports
------------------------------------------------------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:32814->80/tcp, 0.0.0.0:32813->81/tcp, 0.0.0.0:32812->82/tcp
$ docker-compose up -d --force-recreate; docker-compose ps
Recreating dockernginx_nginx_1 ... done
       Name                 Command          State                                  Ports
------------------------------------------------------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:32817->80/tcp, 0.0.0.0:32816->81/tcp, 0.0.0.0:32815->82/tcp
$ docker-compose up -d --force-recreate; docker-compose ps
Recreating dockernginx_nginx_1 ... done
       Name                 Command          State                                  Ports
------------------------------------------------------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:32820->80/tcp, 0.0.0.0:32819->81/tcp, 0.0.0.0:32818->82/tcp
$ docker-compose up -d --force-recreate; docker-compose ps
Recreating dockernginx_nginx_1 ... done
       Name                 Command          State                                  Ports
------------------------------------------------------------------------------------------------------------------------
dockernginx_nginx_1   nginx -g daemon off;   Up      0.0.0.0:32823->80/tcp, 0.0.0.0:32822->81/tcp, 0.0.0.0:32821->82/tcp
```


###  `"80-80"` （範囲の始まりと終わりが同じ番号）
`"80"` 指定と同じ。

###  :x: `"127.0.0.1-5:8080:80"`
`<Host IP>` 部分に範囲指定はできない

```sh
$ docker-compose up -d
The Compose file '.\docker-compose.yml' is invalid because:
services.nginx.ports is invalid: Invalid port "127.0.0.1-5:8080:80", should be [[remote_ip:]remote_port[-remote_port]:]port[/protocol]
```


###  :x: `"127.0.0.1-5:8080-8085:80"`
同上

```sh
$ docker-compose up -d
The Compose file '.\docker-compose.yml' is invalid because:
services.nginx.ports is invalid: Invalid port "127.0.0.1-5:8080-8085:80", should be [[remote_ip:]remote_port[-remote_port]:]port[/protocol]
```

# 参考ページ
- [Compose file version 3 reference | Docker Documentation](https://docs.docker.com/compose/compose-file/#ports)
- [Docker Composeのscaleオプションを使ったときのポートマッピング設定方法 - 理系学生日記](https://kiririmode.hatenablog.jp/entry/20180615/1529069477)