docker-composeを、Dockerコマンドの集合体として理解する【Dockerコンテナ・グレートジャーニー⑤】

# Dockerコンテナグレートジャーニー
Dockerコンテナを０から理解する旅、Dockerコンテナ・グレートジャーニー第5回です。
これまで生Dockerの基礎を学んできました。今回はdocker-composeを用いて複数のコンテナを管理する意味と方法について解説します。
特に、前回までの記事も読まれた方にとっては、復習とDockerコマンドの応用にもなるはずです。

## 対象
* Dockerコマンドの基礎を学び終えた人
* docker-composeについて、概念レベルでの理解をしたい人
* docker-composeを実際に使いたい人

## 旅路（インデックス）
長いので記事を分割しています。
1. [そもそも仮想化とは？　仮想化ではないシステムとは？](https://qiita.com/Toyo_m/items/3060fc5961d62a840bee)
1. [Dockerコマンド基礎（環境構築～ubuntu/httpdコンテナでのコマンド実行）](https://qiita.com/Toyo_m/items/94de894a8558d94b02d1)
1. [Dockerのストレージについて](https://qiita.com/Toyo_m/items/95a2543c07dc5b126208)
1. [Dockerのネットワークについて](https://qiita.com/drafts/52fa81948d5746dd2afc/edit)
1. Docker-compose でコンテナをまとめる<font color=orange>【⇦本記事】</font>
1. [Docker image と Dockerfile](https://qiita.com/Toyo_m/items/5713f85a9d5ff8d9cc0d)
1. [AWS ECS で Dockerコンテナを走らせる](https://qiita.com/Toyo_m/items/90df3c90353ed6fb3fd8)

# 【主題】docker-composeを、Dockerコマンドの集合体として理解する
こんなことはありませんか？
* dockerコンテナを使おうとググった時、Webの記事ではなぜか`docker-compose.yaml`を使って解説されていた。
* そこに書かれていたことをそのままコピペして使っている。
* 結局何をやっているのかよくわからない。

Dockerコマンドの基礎を学ばずにdocker-composeに手を出すと、間違いなくこのような「よくわかんないけど動く」状態に陥ってしまうでしょう。
しかし、Dockerコンテナグレートジャーニーの第五回までご一緒いただいた皆さんは問題なく理解できるはずです。なぜなら、docker-composeは実質的にDockerコマンドの集合体であり、Dockerコマンドを理解していれば難しくはないからです。
さて、これまでに基礎は学び終えたので、今こそ応用となるdocker-composeの理解を深める時です（まだ以前の記事を読んでいない、またはDockerコマンドの理解が不十分な場合は、まずは前記事を参照してください）

## docker-compose とは何か？
docker-composeを一言で言うと、「複数のdockerコンテナをまとめて管理するためのもの」それに尽きます。

例えば下図のような、外部からのリクエストをNginxで負荷分散して2台のサーバーで応答する構成を、コンテナで実装する場合を考えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/96d0b658-fb9d-91a8-90c2-13545c997990.png)

docker-composeを使用しない場合は、
1. docker networkを作成する
1. docker volume を作成する
1. Webサーバーを2つ立ち上げる
1. nginxを立ち上げる

という手順を踏むことになります。ユーザーが1つ1つのコマンドを打っていく必要があって面倒です。
コンテナが3つだけならまだいいでしょう。しかしこれが10,20…と増えていくと、まともにコマンドを打つのが馬鹿らしくなってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/f6de39f0-884d-08e8-0515-292a1f7a4e5c.png)


そこでdocker-compose を使用すると、`docker-compose up -d`と1コマンド打つだけですべてのコンテナが起動してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/416398da-270d-628c-a673-ec0b1a8721aa.png)

もちろん起動時だけではなく、コンテナを停止、破棄するときも1コマンドだけで済みます。
同時に管理するという特性上、それ以外にも数多くのメリットがあります。

なんとなくのイメージがつかめたところで、その威力を体感してみましょう。

## docker-composeを使わずに、複数コンテナの連携を行う
まずはこれまでの記事で学んだことを応用して、docker-composeを使わずに下図の構成を実現してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/f6de39f0-884d-08e8-0515-292a1f7a4e5c.png)

以前の記事の内容を思い出しながら実行していきましょう。
理解には実際にコマンドを打ってみるのが良いですが、時間が無い方は流し読みしてください。

### 1. ネットワークを作る
docker-networkを定義してそこにコンテナをアタッチすれば、IPアドレスだけでなくコンテナ名で通信ができるのでした。
通常、複数のコンテナを連携させる際には定義します。
``` shell
# ネットワークを作成する
docker network create mynetwork
```

### 2. ストレージを作る
第3回で学んだ通り、Dockerコンテナのメモリは揮発性でしたね。コンテナを破棄した後もデータが残るように、Volumeを作成しておきましょう。
``` shell
# ストレージを作成する
docker volume create vol_html1
docker volume create vol_html2
````

### 3. nginxコンテナを立ち上げる(失敗する)
負荷分散にも使われるwebサーバーであるnginxを立ち上げます。
なお、nginxには設定ファイルが必要です。今回は`host/1`のディレクトリのリクエストはweb1に、`host/2`のディレクトリへのリクエストはweb2のサーバーに振り分けるようにします。
ワークフォルダ配下に、下記のようにファイルを配置してください。
```
work
  |--nginx
    |--default.conf
```
``` default.conf
upstream backend {
  server web1:80;
  server web2:80;
}

server {
  listen 80;

  location /1 {
    proxy_pass http://web1/; # 最後のスラッシュを付けて明示的にパスを指定しないと、http://web1/1 にアクセスされて通信ができないので注意
  }

  location /2 {
    proxy_pass http://web2/; # 同上
  }
}
```

ここではnginxについては解説しません。よくわからない方は「2つのサーバーに処理を振り分けてくれるもの」と考えていただければ大丈夫です。
ではnginxコンテナを立ち上げていきます。

``` shell
docker container run -dit --name nginx_1 --network mynetwork -p 8080:80 --mount type=bind,src="$(pwd)"/nginx/,dst=/etc/nginx/conf.d nginx:latest
```
これでnginxコンテナが立ち上がります。が、1つ問題が発生します。コンテナを立ち上げて20秒待つと、コンテナが停止してしまいます。
``` shell
PS C:\Users\******\qiita_docker_compose> docker container ls -a
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS                     PORTS     NAMES
8ed4fd0fe032   nginx:latest   "/docker-entrypoint.…"   18 seconds ago   Exited (1) 7 seconds ago             nginx_1
```

このようになってしまう理由は、nginxが通信すべきweb1およびweb2のコンテナが見つからないからです。nginxはこのような場合にエラーとなってプロセスを終了させます。メインプロセスが終了するということは、コンテナが終了するということでした。
こんな経緯で、コンテナが終了してしまいます。

つまるところ、nginxのコンテナより先に、web1, web2のコンテナを立ち上げなければならない。つまり依存関係が存在することになります。

### 4. httpdコンテナを2つ立ち上げる
というわけで、先にweb1, web2コンテナを立ち上げていきましょう。
使用するイメージは、以前の記事から引き続きhttpd:2.4。コンテナ名を「web1」「web2」としましょう。
``` shell
docker container run -dit --name web1 --network mynetwork --mount type=volume,src=vol_html1,dst=/usr/local/apache2/htdocs httpd:2.4
docker container run -dit --name web2 --network mynetwork --mount type=volume,src=vol_html2,dst=/usr/local/apache2/htdocs httpd:2.4
```
ストレージには最初に作成したvolumeを使用しています。
下記コマンドで起動したことを確認しましょう。
``` shell
docker container ls -a
```

なお今回は、視覚的にweb1とweb2の違いを区別するため、下記のコマンドで応答するHTMLデータを書き換えておきます。
``` shell
docker exec web1 /bin/bash -c "echo '<html><body><h1>It works on web1!</h1></body></html>' >  htdocs/index.html"
docker exec web2 /bin/bash -c "echo '<html><body><h1>It works on web2!</h1></body></html>' >  htdocs/index.html"
```

### 5. nginxコンテナを再起動する
web1, web2コンテナがしっかりと立ち上がっていれば、nginxはきちんと動作するはずです。
先ほど終了してしまったコンテナを再度起動します。
``` shell
docker container start nginx_1
```
20秒ほど待って起動の有無を確認します。
``` shell
docker container ls -a
```

### 6. 疎通テストを行う
これで準備が整ったので、きちんとサーバーにアクセスできるか確認してみます。
Webブラウザを開いてhttp://localhost:8080/にアクセスしましょう。下の画像のようになっていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/120e1af0-bcd0-32f9-83ac-d51dc8a057ce.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/925ce9f6-75ba-5213-78d2-77db23141e74.png)

### 7. コンテナを破棄する
* docker container ls
* docker network ls
  * どのコンテナおよびネットワークを起動したか調べる
* docker container stop nginx_1 web1 web2
* docker container rm nginx_1 web1 web2
* docker network rm mynetwork
* Volumeももう使わないので削除する
* docker volume ls
* docker volume rm vol_html1 vol_html2

これでスッキリしました。面倒ですね。

ここまでは、docker-composeを使わずDockerコマンドのみでWebサーバーを立ち上げてきました。
Dockerコンテナを扱うにあたって、この感覚はとても重要です。最小単位でコンテナがどう動くかを知っていれば、応用する技術への解像度が大きく違ってきます。
ここまでのことがよくわからない場合は、まずは前回までの記事を復習してみてください。

「コンテナがどう動くのかなんとなく理解できた」という方は、次にdocker-composeを使ってみましょう。


## docker-compose を使った場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/416398da-270d-628c-a673-ec0b1a8721aa.png)

準備として、フォルダの下記の場所に`docker-compose.yaml`ファイルを作成してください。
```
work
  |--nginx
    |--default.conf
  |--docker-compose.yaml
```
ファイルの内容は以下をコピペしてください。docker-compose.yamlの説明は後で行うので、ひとまずは体感してみましょう。
``` docker-compose.yaml
version: '3.8'

services:
  nginx:
    image: nginx:latest
    ports:
      - 8080:80
    volumes:
      - type: bind
        source: ./nginx
        target: /etc/nginx/conf.d
    depends_on:
      - web1
      - web2
  web1:
    image: httpd:2.4
    volumes:
      - type: volume
        source: html1
        target: /usr/local/apache2/htdocs/
  web2:
    image: httpd:2.4
    volumes:
      - type: volume
        source: html2
        target: /usr/local/apache2/htdocs/

volumes:
  html1:
  html2:
```

では起動します。
``` shell
docker-compose up -d
```
以上です。これで3つのDockerコンテナと、ネットワークと、ストレージが起動しました。煩わしい操作はなく、個別にコンテナを立ち上げるのは面倒でしたが、docker-composeを使えばこれだけです。

なお今回は、視覚的にweb1とweb2の違いを区別するため、下記コマンドでコンテナが応答するHTMLデータを書き換えておきます。
``` shell
docker-compose ps
# 注意：docker-compose で立ち上げたコンテナは、名前にプレフィックスが付くので、`docker exec <コンテナ名>`のコンテナ名の部分は適宜書き換えてください
docker exec qiita_docker_compose-web1-1 /bin/bash -c "echo '<html><body><h1>It works on web1!</h1></body></html>' >  htdocs/index.html"
docker exec qiita_docker_compose-web2-1 /bin/bash -c "echo '<html><body><h1>It works on web2!</h1></body></html>' >  htdocs/index.html"
```

### 疎通確認
先ほどと同じように、localhostにアクセスしてコンテナが正しく動いているか確認しましょう。
下図のようになっていればOKです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/120e1af0-bcd0-32f9-83ac-d51dc8a057ce.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/925ce9f6-75ba-5213-78d2-77db23141e74.png)

### コンテナを破棄する
検証が終わったのでコンテナを破棄します。
破棄するコマンドは、
```
docker-compose down
```
以上です。とても簡単ですね。
コンテナやネットワーク、それぞれの動作状況はdocker-composeが裏側でしっかり管理しているので、このコマンドだけで`docker-compose up -d`で起動したものをきれいに削除してくれます。（※Volumeは削除されていません）

これで、docker-composeによるコンテナ立ち上げから破棄までを体験できました。
では次に、docker-composeの動作を定義していたdocker-compose.yamlファイルについて見ていきます。

## docker-compose.yaml解説
ここからは、docker-compose.yaml内の1つ1つの記述について解説していきます。
yamlファイルは、「`:`」とインデントによって分割、親子構造を表現するファイル形式です。

```
# docker-composeのバージョン指定です。
version: '3.8'
```

```
# コンテナの親部分
services:
```
```
  # コンテナ名。
  # 外から見るとプレフィックスがついた名前になりますが、同一docker-compose内のコンテナからは`nginx`として認識されます
  nginx: 
```
```
    #Dockerイメージを指定しています。
    image: nginx:latest
```

```
    # docker コマンドの　-p 8080:80　と同じです。ホストの8080とコンテナの80ポートを接続します
    ports:
      - 8080:80
```
```
    # dockerコマンドの --mount type=bind,src=***,dst=*** と同じです。
    volumes:
      - type: bind
        source: ./nginx
        target: /etc/nginx/conf.d
```
```
    # 依存関係。
    # これを書くことで、コンテナが[web1, web2 -> nginx]の順に起動することができます。
    # これにより、nginxが接続先を見つけられないエラーを回避できます。
    # 加えて、docker-composeを停止するとき、[nginx -> web1, web2] の順番で停止することができます。
    # 注) コンテナの起動順序を制御するだけで、コンテナ内部のサービス起動まではチェックできません
    depends_on:
      - web1
      - web2
```

ここから下はweb1コンテナ
``` yaml
  # コンテナ名
  web1:
    # Dokcerイメージの指定
    image: httpd:2.4
    # docker コマンドの`--mount type=volume,src=html1,dst=/usr/local/apache2/htdocs/`と同じです
    volumes:
      - type: volume
        source: html1
        target: /usr/local/apache2/htdocs/
```

（※web2についてはweb1と全く同じなので省略します）

いかがでしょうか？
コマンドを細かく見ていくと、dockerコマンドで実行していたことと同じことをやっています。というより、dockerコマンドでのコンテナ起動が元になっているので当然ですね（dockerコマンドを忘れている場合は、本記事の第2~4回目を再度参照ください！）。

また、docker-compose内ではネットワークを明示的に定義をしていません。しかしdocker-composeを使用して作られたコンテナは、起動時に自動的に作られたネットワークに参加しています！　Dockerコマンドのみで作業をしていた時と比べて、ネットワークを定義する手間が省けるのです。
なお、自動作成されたネットワークは`docker-compose down`を実行したときに自動で削除されているので、我々はそもそもネットワークの存在を気にしなくても通信ができます。

## まとめ
* docker-composeは、複数のコンテナをまとめて管理できる
* yamlファイルによって記述されるが、実態は、Dockerコマンドの集合体
* ネットワーク設定をしなくても、自動で定義してくれる
* `depends_on`で、コンテナの依存（起動/停止の順番）を定義できる

いかがでしたでしょうか？
docker-composeをDockerコマンドレベルから理解することで、そのメリットと利点をより享受できるようになるはずです。
いいことずくめなツールですので、ぜひ使っていきましょう！


## 参考：その他docker-compose のコマンド
`docker-compose ~~` のように以下のコマンドを使えます。
* stop / start
    * `docker-compose -down`はコンテナを終了＆破棄までしてしまうのに対して、`stop`破棄しません。
    * `start`は、`stop`したコンテナ群を再開できます。
* ps
    * 該当docker-composeで起動中のコンテナ一覧を確認できます。
* exec
    * Dockerコマンドと同様、任意のコマンドを実行できます
* logs
  * それぞれのコンテナに対してログを確認することができます。
* top
  * プロセスの確認
* --scale
    * コンテナの起動数を指定できる。例えば、web1コンテナの性能が足りないので、3台に増やしたい！　という設定もできます。
    * `--`を付けない`scale`は非推奨になっているので注意


# 参考資料


最初はこちらの本で勉強しました

https://www.amazon.co.jp/%E3%81%95%E3%82%8F%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B6%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9-docker%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E6%A7%8B%E7%AF%89-%E5%A4%A7%E6%BE%A4-%E6%96%87%E5%AD%9D/dp/4296106422

depends_on について載っている公式ドキュメントです。

https://docs.docker.com/compose/compose-file/compose-file-v3/#depends_on