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

Dockerネットワーク

タイトルは釣りです

1677万案件は試してません

対象読者

  • 開発環境を docker-compose で構築しているWebプログラマの人々
  • 複数案件を抱え、ホスト側ポート番号を考えるのに疲弊している人々

    • HTTP 80 -> 80はとっておいて8080にしたろ!
    • あの案件で8080を使ったからこっちは10080にしたろ!
    • 10080の次だからとりあえず20080にしたろ!
    • うーん、8000!w
    • HMRで8080使いたいけれど使われてるから8081にしたろ!

問題提起

docker-compose.ymlの例

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)

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

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を導入したくなりました。

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で…

anken-2/docker-compose.yml

...
  app:
    image: "httpd:2.4"
    ports:
      - "8080:80"
...

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

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で管理してくれ」

となります。

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

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

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

  • 「何箇所設定するね〜んww」
  • 「何もしてないのに docker-compose up -d が立ち上がらなくなった」

    • env_exampleの更新を.envへ反映し忘れている

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

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

  • サンプル

    • レポジトリ名をdocker-composerにしてしまう痛恨のミス

[https://github.com/wand2016/docker-composer-multiple-projects-sample:embed:cite]

公式docに書いてありますが、docker run -pではホスト側ポートをバインドするIPアドレスを指定できます。

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

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

往来で

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

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

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

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

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

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

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.1127.255.255.254 は全て別のIPアドレスであり、個々にポートをバインドできます。

これを使って、

「IP=127.0.0.1は案件1用」

というようにして、803306といった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との違い

[https://hub.docker.com/r/jwilder/nginx-proxy/:embed:cite]

?「nginx-proxyでいいんじゃ?」

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

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

macOS固有の問題

Docker Desktop for mac 2.2.x の不具合

[https://github.com/docker/for-mac/issues/4209:embed:cite]

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:embed:cite]

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

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

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