Docker for Macでリバースプロキシを使わずに同じポートで待受するコンテナを複数起動する

概要

Docker for Macを使っていて、80番ポートのコンテナを複数起動したいとする。
この場合、最初に起動したコンテナはデフォルトで0.0.0.0:80で待受するので、2つ目を起動しようとすと、

docker: Error response from daemon: driver failed programming external connectivity on endpoint nginxB (1c90fd41c712e357ef339bfd99ccda3258b20f5955ec8583a52c18bdaf643d91): Bind for 0.0.0.0:80 failed: port is already allocated.

と言ったエラーになって、起動できない。
通常、こうならないように前段に80番で待受するNginxなどを使って、リバースプロキシするか、10080:80の様に待受ポートを変えるのが普通だと思う。
しかし、なんとかして80番ポートを待ち受けするコンテナを複数起動する良い方法がないか探したところ、一応方法があったので記録しておく。

方法1: 割当られているネットワークのIPアドレスを複数使う

例えば、WiFiを使ってインターネットに接続していたとすると、基本的にはDHCPでIPアドレスが振られる。
同じネットワーク帯のIPアドレスをもう一つもらってしまう方法で上記エラーが解決できる。

まず、Macで複数IPを設定する。
WiFiだったら既存のWiFiインタフェースを選択して複製をする。

次に手動でIPアドレスを設定する。

適用すると下記のようにIPアドレスが振られる。(IPは適当)

$ ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        ether a8:66:7f:1c:35:e9
        inet6 fe80::84b:528c:e2d6:a4cc%en0 prefixlen 64 secured scopeid 0x5
        inet 192.168.200.244 netmask 0xffffff00 broadcast 192.168.200.255
        inet 192.168.200.254 netmask 0xffffff00 broadcast 192.168.200.255
        nd6 options=201<PERFORMNUD,DAD>
        media: autoselect
        status: active

IPアドレスを2つ所有したら、下記のように実行する。

$ docker run -dti --name nginxA -p 192.168.200.244:80:80 -d nginx
$ docker run -dti --name nginxB -p 192.168.200.254:80:80 -d nginx

そうすると下記のように待ち受けができるようになる。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
b1e139ccd611        nginx               "nginx -g 'daemon of…"   15 minutes ago      Up 15 minutes       192.168.200.254:80->80/tcp   nginxB
598a1a2dbcf5        nginx               "nginx -g 'daemon of…"   16 minutes ago      Up 16 minutes       192.168.200.244:80->80/tcp   nginxA

もちろんブラウザでそれぞれのIPアドレスで接続できるようになる。
ただ、IPアドレスを自由に割り振りできる自宅のネットワークだからできるのであって、会社では使えないだろうなと思う。

方法2: lo0にaliasでIPアドレスを割り当てて使う

lo0には127.0.0.1が振られているが、そのインターフェースのaliasにIPアドレスを振ってしまう。

$ sudo ifconfig lo0 alias 192.168.200.1/24
$ sudo ifconfig lo0 alias 192.168.201.1/24
$ ifconfig lo0
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
        options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
        inet 127.0.0.1 netmask 0xff000000
        inet6 ::1 prefixlen 128
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
        inet 192.168.200.1 netmask 0xffffff00
        inet 192.168.201.1 netmask 0xffffff00
        nd6 options=201<PERFORMNUD,DAD>

上記ネットワーク帯でdockerのnetworkを作成する。

$ docker network create --subnet 192.168.200.1/24 --gateway 192.168.200.1 mynetworkA
$ docker network create --subnet 192.168.201.1/24 --gateway 192.168.201.1 mynetworkB
$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
b607e1b256d3        bridge              bridge              local
1a9676066305        host                host                local
c9b3bc8f2c03        mynetworkA          bridge              local
f4c1995c0da5        mynetworkB          bridge              local
20562a1fec5d        none                null                local

そうしたら、docker起動時の公開ポートにIPアドレスを指定して、runしてあげる。

$ docker run -dti --name nginxA --net=mynetworkA -p 192.168.200.1:80:80 --ip=192.168.200.2 nginx
$ docker run -dti --name nginxB --net=mynetworkB -p 192.168.201.1:80:80 --ip=192.168.201.2 nginx
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
8f3b951723a6        nginx               "nginx -g 'daemon of…"   28 seconds ago      Up 27 seconds       192.168.200.1:80->80/tcp   nginxA
bc36df351589        nginx               "nginx -g 'daemon of…"   54 seconds ago      Up 53 seconds       192.168.201.1:80->80/tcp   nginxB

そうするとエラー無くnginxが80番ポートで2個起動する。もちろん、ブラウザからIPアドレスを指定すれば、接続できるし、各コンテナからもインターネットに出ていくことができる。

$ curl -I http://192.168.200.1
HTTP/1.1 200 OK
Server: nginx/1.15.3
Date: Sun, 23 Sep 2018 11:06:05 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 28 Aug 2018 13:32:13 GMT
Connection: keep-alive
ETag: "5b854edd-264"
Accept-Ranges: bytes

$ curl -I http://192.168.201.1
HTTP/1.1 200 OK
Server: nginx/1.15.3
Date: Sun, 23 Sep 2018 11:06:08 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 28 Aug 2018 13:32:13 GMT
Connection: keep-alive
ETag: "5b854edd-264"
Accept-Ranges: bytes

LoginHookの設定

ifconfigでalias指定した場合は、Macの再起動時に設定が消えてしまう。
Macの場合、LoginHookというログイン時にスクリプトを実行できる便利ワザがあるので、それで起動時にifconfigを実行するようにしてあげる。

$ vim ~/.lo0ip.sh
#!/bin/bash

sudo ifconfig lo0 alias 192.168.100.1/24
sudo ifconfig lo0 alias 192.168.101.1/24
$ chmod u+x ~/.lo0ip.sh

上記スクリプトをホーム直下に置いて、defaultsでwriteする。

$ sudo defaults write com.apple.loginwindow LoginHook ~/.lo0ip.sh

設定確認は下記コマンドで可能。

$ sudo defaults read com.apple.loginwindow LoginHook
/Users/kazuma/.lo0ip.sh

これで、再起動しても常にlo0にaliasが設定されるようになる。

まとめ

会社でちょっと要望があったので、なんとかならないか調べたら、こんな方法でいけそうかなぁという感触を得た。
とはいえ、実際にみんなに同じことをさせるのはいささか仰々しいような気がするので、実際にやるかは応相談だと思う。
しかし、お陰でdockerのnetwork周りが少しだけ勉強になったので、個人的には得るものはあった。
あと、MacのLoginHookも便利だなと思う。
以前Macで定期実行させるのにCronじゃなくてLaunchdを使うという記事で、スクリプトを定期実行させていたけど、LoginHookのほうがはるかに便利だと思った。

おまけ

lo0のaliasを解除するには下記で解除可能。

$ sudo ifconfig -alias 192.168.200.1

LoginHookの解除。

$ sudo defaults delete com.apple.loginwindow LoginHook
$ sudo defaults read com.apple.loginwindow LoginHook
2018-09-23 21:23:43.996 defaults[2379:18698]
The domain/default pair of (com.apple.loginwindow, LoginHook) does not exist

dockerネットワークの確認。

 docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
e864f9db3ea2        bridge              bridge              local
1a9676066305        host                host                local
c9b3bc8f2c03        mynetworkA          bridge              local
f4c1995c0da5        mynetworkB          bridge              local
20562a1fec5d        none                null                local

dockerネットワークの詳細を確認。

 docker inspect mynetworkA
[
    {
        "Name": "mynetworkA",
        "Id": "c9b3bc8f2c039fe70cd1359ecf88cf902f66665d27edaf78cdc3beb2c0a6bc10",
        "Created": "2018-09-23T10:57:41.803838Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.168.200.0/24",
                    "Gateway": "192.168.200.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "8f3b951723a69ba97fb702e327885187d88ab1b3cbb3a7ab205898df726e8596": {
                "Name": "nginxA",
                "EndpointID": "631fea0b6c656f99a310828578ef2d50a6a710e9e8a9918b1904f8b19a0ea6c0",
                "MacAddress": "02:42:c0:a8:c8:02",
                "IPv4Address": "192.168.200.2/24",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

参考リンク
Docker version: Running 2 sites/containers at same time · Issue #1393 · geerlingguy/drupal-vm · GitHub
Macのログインフックを利用して自動でlo0のipアドレスを割り振る – joppot