これまでのDocker Compose入門は、Docker Compoesの紹介に始まり(第1回)、簡単なウェブサーバを起動する方法(第2回)、ネットワークの理解を深める方法(第3回)でした。今回は連載のまとめとして、ネットワークとボリュームの活用によって、1つのサーバもしくはPC上に複数のアプリケーション環境を動かす方法をみていきいましょう。

Docker Composeはプロジェクトごとにネットワークを持つ

前回はDockerのネットワークの扱いを学びました。Dockerコンテナを実行すると、そのコンテナ内のプロセスは、デフォルトで「bridge」という名称の仮想的な内部ネットワークを通して通信を行います。またホスト上のブリッジ(仮想的なネットワーク・スイッチ)を経由して、Dockerが作成する他の内部ネットワークや、ホスト側のインターフェースを通して、インターネットなどの外部ネットワークと通信可能になります。

Docker Composeでは、docker-compose.ymlというYAMLファイルで定義するコンテナ(サービス)やネットワーク、ボリュームなどをプロジェクトと呼びます。デフォルトでは、YAMLファイルがあるディレクトリ名がプロジェクト名になります。プロジェクト名はディレクトリ名です。例えば、「dev」というディレクトリ内にYAMLファイルを置くとしますと、これを使ったサービス等を「devプロジェクト」と呼びます(補足:docker-compose -pオプションを使い、重複しない任意のプロジェクト名の指定もできます)。

ポイントは、このプロジェクト単位でブリッジ・ネットワークを分けられる点です。Docker Compoesは、YAMLファイルの内容が同じだとしても、それを実行するディレクトリ(プロジェクト名)が異なっていれば、お互いのプロセスが隔離されているだけでなく、ネットワークにも影響を与えずに実行できます。通常の「docker run」コマンドでは1つ1つのアプリケーションを隔離(isolate)して実行すると、お互いに影響を与えず実行できます。それと同じように、Docker Composeでは「docker-compose up」コマンドの実行によって、1つ1つのアプリケーションをお互い何ら影響を与えずに実行できます。

Docker コンテナと Docker Compose のネットワーク比較図

便利な使い方としては、1つのプロジェクト内で、複数のネットワークを使い分けるものがあります。次のようなYAMLファイルを準備して、動作を確認してみましょう。

version: "3"
services:
  web:
    image: alpine
    command: ping 127.0.0.1
    networks:
      - frontend
  middle:
    image: alpine
    command: ping 127.0.0.1
    networks:
      - frontend
      - backend
  db:
    image: alpine
    command: ping 127.0.0.1
    networks:
      - backend
networks:
  frontend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.10.0/24
  backend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.20.0/24

このプロジェクトは「web」「middle」「db」という3つのサービスで構成されています。それぞれAlpine Linuxのイメージを用い、自らのインターフェースに対してpingを実行するものです。これはdocker-compose up -dコマンドで起動できます。実行後、ネットワークの一覧を確認するコマンドを実行すると「プロジェクト名_ネットワーク名」の形式でプロジェクト内部通信用のブリッジ・ネットワークが作成されていることが分かります。つまり、自動的に「mynet_backend」と「mynet_frontend」ネットワークが作成されました。

# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
37740fe8df0d        bridge              bridge              local
acd0c7851c0a        host                host                local
396ac141a025        mynet_backend       bridge              local           ←追加されたブリッジ・ネットワーク
bc96fee76265        mynet_frontend      bridge              local           ←追加されたブリッジ・ネットワーク
33a7ac31ca4c        none                null                local

ここで注目するのは「networks:」のセクションです。それぞれのサービスは、「frontend」または「backend」あるいはその両方に属しています。こちらを整理すると、下図のようなネットワーク接続になります。

つまり一般的なアプリケーションのように、フロントエンド側のサーバがバックエンドと直接接続できないような環境を作るのも Docker Compose では容易です。また、この例では分かりやすくするために「- subnet:」サブセクションでネットワーク範囲を定義していますが、こちらを指定しなくてもdocker-compose upは動作します。なぜなら、Docker Composeのネットワークはサービス名を使ってIPアドレスの自動的な名前解決が可能だからです。

今回の例では「web」「middle」「db」というサービス名をコンテナに割り当てています。そのため、「web」から「frontend」に対して通信する時にIPアドレスを知る必要はありません。サービス名である「frontend」を指定するだけで動作します。docker execコマンドを使い、mynet_web_1コンテナからmiddleに対してpingを実行してみます。

$ docker exec mynet_web_1 ping -c 3 middle
PING middle (192.168.10.3): 56 data bytes
64 bytes from 192.168.10.3: seq=0 ttl=64 time=0.231 ms
64 bytes from 192.168.10.3: seq=1 ttl=64 time=0.190 ms
64 bytes from 192.168.10.3: seq=2 ttl=64 time=0.204 ms
--- middle ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.190/0.208/0.231 ms

なおmynet_web_1からdbに対してはpingが通りません。これはお互いが異なるブリッジ・ネットワークに接続しているため名前解決できないからです。

$ docker exec mynet_web_1 ping -c 3 db
ping: bad address 'db'

その一方、middleはどちらのネットワークにも所属していますので、webに対してもdbに対してもpingが疎通し、かつ名前解決ができています。

$ docker exec mynet_middle_1 ping -c 3 web
PING web (192.168.10.2): 56 data bytes
64 bytes from 192.168.10.2: seq=0 ttl=64 time=0.136 ms
64 bytes from 192.168.10.2: seq=1 ttl=64 time=0.154 ms
64 bytes from 192.168.10.2: seq=2 ttl=64 time=0.167 ms
--- web ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.136/0.152/0.167 ms
$ docker exec mynet_middle_1 ping -c 3 db
PING db (192.168.20.2): 56 data bytes
64 bytes from 192.168.20.2: seq=0 ttl=64 time=0.152 ms
64 bytes from 192.168.20.2: seq=1 ttl=64 time=0.190 ms
64 bytes from 192.168.20.2: seq=2 ttl=64 time=0.194 ms
--- db ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.152/0.178/0.194 ms

このあたりの挙動は少々不思議に見えるかもしれません。ですが、Docker Composeを使ったネットワーク構築には欠かせない基本的な考えと動作が以上の手順に詰まっていますので、皆さんご自身が納得するまで手を動かしながら挙動を確認し、理解されることをおすすめします。

この、Docker Composeによって作成されたブリッジ・ネットワークは、「docker-compose down」コマンドの実行によって自動的に削除されます。そのままDocker Compose上のサービスとブリッジ・ネットワークを使い続ける場合は支障ありませんが、ネットワークだけ残し続けることはできないため、注意が必要です。

Composeのサービス間で共有するデータは、ボリュームに保存

Dockerでは「ボリューム」と呼ぶ機能を使い、コンテナ内に記録しないデータを保管したり、ホスト上のファイルやディレクトリをマウントするために利用できます。ボリュームとは正確には、Dockerイメージを構成するイメージ・レイヤの制約を受けることなくコンテナ内のファイルシステム上に存在する領域であり、Dockerコンテナやイメージとは独立し、Dockerホスト上のファイルシステムと直接やりとりできる機能です。そのため、動的なデータやログファイルの保存や一時的なファイルを読み書きする場所として用いられます。

多くの公式イメージでは、Dockerイメージの中で予めボリュームの領域が定義されています。そのため、おそらく普段意識することなくボリュームを使っている可能性が高いでしょう(例えばコマンド「docker volume ls」を実行すると、気づかないうちに多くのディレクトリが作成されているかもしれません)。

ここではシンプルに、Docker Composeプロジェクト内でのみデータ共有が可能なボリューム作成を考えてみます。まず、「mydata」というディレクトリを作成し、移動した後に以下のdocker-compose.ymlファイルを作成します。

version: "3"
services:
  web:
    image: alpine
    command: ping 127.0.0.1
    volumes:
      - share:/data
  db:
    image: alpine
    command: ping 127.0.0.1
    volumes:
      - share:/data
volumes:
   share:

これは「web」「db」という各サービス(コンテナ)の中で、「share」という名前付きボリュームを「/data」にマウントしています。さらに「volumes:」セクションでは「share」という名前の共通する名前付きボリュームを作成するものです。これを使えば、両方のサービスは「/data」を使ってデータを共有できます。こちらを使い「docker-compse up -d」を実行しましょう。そうすると、自動的に「プロジェクト名_share」という名前付きボリュームが作成されます。今回の場合ではボリューム一覧表示コマンドを実行すると、次のような結果になります。

$docker volume ls
DRIVER              VOLUME NAME
local               mydata_share

動作確認のため、ファイルをホスト上のボリューム上に直接作成してみましょう。「mydata_share」名前付きボリュームの実体はデフォルトで「/var/lib/docker/volumes/mydata_share/_data/」になります。ここに「hello.txt」を書き出します。

# echo "hello" > /var/lib/docker/volumes/mydata_share/_data/hello.txt

次に、webとdbのコンテナの中で、このファイルが見えるかどうかを確認します。

# docker exec mydata_web_1 ls -al /data
total 12
drwxr-xr-x    2 root     root          4096 Oct 20 02:47 .
drwxr-xr-x    1 root     root          4096 Oct 20 02:37 ..
-rw-r--r--    1 root     root             6 Oct 20 02:47 hello.txt
# docker exec mydata_db_1 ls -al /data
total 12
drwxr-xr-x    2 root     root          4096 Oct 20 02:47 .
drwxr-xr-x    1 root     root          4096 Oct 20 02:37 ..
-rw-r--r--    1 root     root             6 Oct 20 02:47 hello.txt

このように、Docker Composeを使えば簡単にサービス間で共有するボリュームを作れるのも簡単です。

ここで先ほどのネットワークを振り返ってみましょう。Docker Composeでネットワークを定義すると「プロジェクト名_ネットワーク名」のブリッジ・ネットワークが自動作成されました。それと同じようにDocker Composeで自動作成するボリューム名は「プロジェクト名_ボリューム名」の形式となります。そのため、1つのDocker Composeファイルがあれば、同じホスト上でありながら、イメージ名やタグ、環境変数など少し設定を変えたサービスやコンテナを起動するのが簡単になります。なお、実際にはホスト側にマッピングするポートは(コンテナと同様に)複数のプロジェクトで重複できませんし、ホスト上のディレクトリを直接ボリュームとしてバインドする場合には注意が必要です。それでも、お互いのプロジェクトに影響を与えないよう、うまく工夫することもできますので、開発環境の再現やテストなど、利用形態にとってはメリットになるでしょう。

ここからは自分の開発環境やアプリケーションの Docker 化を目指してみよう

これまでの連載を通し、アプリケーションのコンテナ化に必要な概念と、Docker Composeの基本的な使い方を学びました。何もない状態からDocker Composeを使い始めるのは大変です。ですが、Dockerのコンテナやイメージ、そしてネットワークとボリュームの基本概念を理解した今となっては、決して高くないハードルになっていると思います。

今後、Docker Composeで多くのことを実現するには、Composeファイルの書式についての理解が必要になります。これも、いきなり全てを読み込むのは大変ですので、必要になった場合にリファレンスを見ながら確認されることをおすすめします。また、最近では様々な GitHub 上のプロジェクトでDockerfileとdocker-compose.ymlファイルの配布が増えてきています。他にもAwesome Compose(すごいCompose、の意味)というサンプル集も公開されています。

これらを参考にしながら、自分の利用にあったベストなDocker Compose活用方法を模索していただければと思っています。