【Go】ログイン機能でウェブアプリを作ってみる（4）

こんにちは。

Part 4はdocker-compose.ymlの解説についてです。

# 今回の目標
- docker-compose.ymlをなんとなく理解しよう！
- docker compose コマンドの使い方を覚えよう！

## なんでdockerを使うの？

前回 docker-compose.yml を作成し、MySQLコンテナを起動しました。しかし

なんでわざわざこんなことをするのでしょうか？

理由の一つとして 「ローカル（自分のPC）からの影響を排除したい」 と言うのがあります。
例えばコンテナを起動せずにもともとPCに入っているMySQLアプリを使ったとします。
自分のPCのMySQLは日本語対応が済んでいる、とします。
開発中は日本語でも問題ありません。すでに日本語対応したMySQLを使っているからです。
しかし、実際のサーバーにMySQLを入れる時に日本語対応ができておらず、アプリを実行するとエラー。
だったら「初めから」の状態のもの、実際のサーバーとできるだけ近いものを準備し、そこから始めた方が後々の問題を回避できます。

だから「自分のPCからの影響を排除したい」ためにDockerを使います。

## docker composeを使うのはなんで？

ぶっちゃけ楽だからです。
今回は３つのコンテナを使いますが、docker composeがなければ

```
$ docker container run --name api \
	--rm \
	--ごちゃごちゃごちゃごちゃ
$ docker container run --name db \
	--rm \
	-- ごちゃごちゃごちゃごちゃ
$ docker container run --name mail \
	-- ごちゃごちゃごちゃごちゃ
```

とそれぞれのコンテナを起動しないといけません。

docker composeを使えばdocker-compose.ymlを元に全部いい感じにやってくれます。
docker composeは楽です。楽をするためです。

## docker compose コマンドでよく使うやつ

### サービスの起動
```$ docker compose up
$ docker compose up -d
# -d バックグラウンドで起動
```

### サービスのログを確認
```
$ docker compose logs -f
# -f ログを出力し続けてくれます。とりあえずつける、でOK
$ docker compose logs [サービス名] -f
# docker compose logs api -f でapiサービスのログだけ確認できます。
```

### サービスの終了
```
$ docker compose down
```

### サービスの再起動
```
$ docker compose restart
$ docker compose restart [サービス名]
# docker compose restart api でapiサービスを再起動させます。
```

### サービスのコマンド実行
```
$ docker compose exec [サービス名] [コマンド]
# docker compose exec api bash でapiサービスにbashを起動し接続します。
```

## docker-compose.ymlの説明

Part 3で作成したdocker-compose.ymlが次の通りです。
これを元に解説していきます。

```
version: "3.8"

services:
  api:
    container_name: login-example-api
    build:
      dockerfile: Dockerfile
      context: .
    volumes:
      - ".:/app"
    ports:
      - "8000:8000"
    environment:
      DB_USER: login-user
      DB_PASSWORD: login-pass
      DB_NAME: login-db
      DB_HOST: db
      DB_PORT: 3306
    depends_on:
      db:
        condition: service_healthy
  db:
    container_name: login-example-db
    image: mysql:8.0.33
    platform: linux/amd64
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: login-db
      MYSQL_USER: login-user
      MYSQL_PASSWORD: login-pass
    ports: 
      - "33306:3306"
    volumes:
      - type: volume
        source: login-example-db
        target: /var/lib/mysql
      - type: bind
        source: ./_tools/mysql/conf.d
        target: /etc/mysql/conf.d
      - type: bind
        source: ./_tools/mysql/init.d
        target: /docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD", "mysqladmin", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
  mail:
    container_name: login-example-mail
    image: mailhog/mailhog
    platform: linux/x86_64
    ports:
      - "8025:8025"
      - "1025:1025"
volumes:
  login-example-db:
```

### サービス

```
services:
  api:
    ~~~
  db:
    ~~~
  mail:
    ~~~
```
docker composeで「サービスの〜、サービスに〜」と書きましたがこれです。
単なる名前です。今回はapiサービスとdbサービスとmailサービスがあります。
コンテナ１つにつきサービスが１つ対応します。

### container_name
```
services:
  api:
    container_name: login-example-api
    ~~~
  db:
    container_name: login-example-db
    ~~~
  mail:
    container_name: login-example-mail
    ~~~
```
container_nameは、そのままコンテナの名前です。
例えば今回の場合、
apiサービスはlogin-example-apiと言う名前のコンテナと、
dbサービスはlogin-example-dbと言う名前のコンテナと、
mailサービスはlogin-example-mailと言う名前のコンテナと対応しています。
「別の名前つけて意味あるの？」って思うかもしれません。
使い分けとしては

- docker composeコマンドからはサービス名を指定する
- docker containerコマンドからはコンテナの名前を指定する

そんな感じです。

### build & image
```
services:
  api:
    ~~~
    build:
      dockerfile: Dockerfile
      context: .
    ~~~
  db:
    ~~~
    image: mysql:8.0.33
    ~~~
  mail:
    ~~~
    image: mailhog/mailhog
    ~~~
```

サービスの中身です。
例えばdbサービスはimage: mysql:8.0.33なので、中身はMySQLの8.0.33です。
apiサービスみたいにDockerfileで中身を作成したい場合 build を使います。

### environment
```
services:
  api:
    ~~~
    environment:
      DB_USER: login-user
      DB_PASSWORD: login-pass
      DB_NAME: login-db
      DB_HOST: db
      DB_PORT: 3306
    ~~~
  db:
    ~~~
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: login-db
      MYSQL_USER: login-user
      MYSQL_PASSWORD: login-pass
    ~~~
  mail:
    ~~~
    ~~~
```

それぞれのサービス内で環境変数を定義してます。

確認してみます。
```
$ docker compose up -d
# apiサービスに接続してみます。
$ docker compose exec api bash

app/ $ echo $DB_USER
login-user
# 環境変数がちゃんと定義されているのが確認できます。

app/ $ echo $MYSQL_USER

# 何もなし。apiサービスにはdbサービスの環境変数はもちろん定義されてません

app/ $ exit
$ docker compose down
```

また、
前回dbサービスのMySQLに接続するときに次のコマンドを実行しました。

```
$ docker compose exec db mysql -u login-user -plogin-pass login-db
```

なんでこれで接続できたか、というとMySQLのコンテナはこのenvironmentで定義された環境変数を元にMySQLのユーザーなどを事前に作成してくれたからです。

### volumes

```
services:
  api:
    ~~~
    volumes:
      - ".:/app"
      # - ".:/app"は次と同じ意味です
      # - type: bind
      #   source: .
      #   target: /app
    ~~~
  db:
    ~~~
    volumes:
      - type: volume
        source: login-example-db
        target: /var/lib/mysql
      - type: bind
        source: ./_tools/mysql/conf.d
        target: /etc/mysql/conf.d
      - type: bind
        source: ./_tools/mysql/init.d
        target: /docker-entrypoint-initdb.d
      # 次のように書いてもOK
      # - login-example-db:/var/lib/mysql
      # - ./_tools/mysql/conf.d:/etc/mysql/conf.d
      # - ./_tools/mysql/init.d:/docker-entrypoint-initdb.d
    ~~~
  mail:
    ~~~
    ~~~
volumes:
  login-example-db:
```
サービス内のデータをいい感じに使いたい時に使います。
２つのtypeがあります。
- サービス内のデータは基本使い捨て。サービスを停止したらサービス内のデータは削除される。
- type: volume
    - サービス内のデータを残しておきたいときなんかに使う。
    - 例えばテスト用にデータを10万件作成したが、毎回サービスを起動するたびに10万作成するのは面倒くさい、データを使い回したい、みたいな時とかに便利
    - 今回はMySQLのデータを使い回すのに使ってます
- type: bind
    - コードなど自分のPC内のデータをサービス内で使いたい時に使う
    - コードを修正したらコンテナ内のコードも同じように修正されてて欲しい、そんな時に使う
    - 今回はGoのコードをapiサービス内に反映させたり、MySQLの設定ファイルをdbサービス内に反映させたり、に使ってます。

### ports
```
services:
  api:
    ~~~
    ports:
      - "8000:8000"
    ~~~
  db:
    ~~~
    ports: 
      - "33306:3306"
    ~~~
  mail:
    ~~~
    ports:
      - "8025:8025"
      - "1025:1025"
    ~~~
```

自分のPCのポート番号：サービス内のポート番号を対応させています。
例えばdbサービスの33306:3306というのは自分のPCの33306ポートがdbサービスの3306ポートに対応している、ということです。

### depends_on
サービスの起動順序を決めています。
今回のapiサービスのGoのコードではMySQLに接続できなければエラーになるようになっています。
なのでdbサービスのMySQLが接続できるようになってからapiサービスを起動するようにしたい、そんな時に使います。
上記のように記述するとdbサービスが起動してからapiサービスが起動するようになります。
ただ、これだと
- （期待する順番）dbサービスが起動 > MySQLが起動し、接続準備OK > apiサービスが起動しMySQLに接続
- （実際の順番）dbサービスが起動 > MySQLが起動 > apiサービスが起動しMySQLに接続 > MySQLはまだ接続準備ができていないのでエラー

みたいなことになります。
なので、ちょっと変更して

```
services:
  api:
    ~~~
    depends_on:
      db:
        condition: service_healthy
    ~~~
  db:
    ~~~
    healthcheck:
      test: ["CMD", "mysqladmin", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
    ~~~
  mail:
    ~~~
    ~~~
```

こんな感じにします。
dbサービスがどうなったらapiサービスが起動するか、という条件を加えました。
こうすればMySQLが接続準備OKになってからapiサービスが起動するようになります。

今回はdocker-compose.ymlの解説をしました。

今日は以上です。
ありがとうございました。

次のPart 5は仮登録 /auth/register/initial （１）の解説です。
よろしくお願いいたします。