DockerでMySQL・Node.jsコンテナを用いたToDoリストアプリを作成する

# はじめに

Dcokerの使い方を学んだので、Docker上でNode.jsアプリケーションを作ってみました。
非常に多くの記事やサイトを参考にして作ったので、自分用のまとめ的な側面もあります。
作成したのは簡単なToDoリストアプリです。

初心者なので色々おかしな点があると思いますが、気づいたらコメントで教えていただけるとありがたいです。

# 完成イメージ

CRUD機能を搭載した簡単なToDoリストを作ります。
完成イメージは以下の通りです。

![ToDoリストアプリの完成形.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/84a5ed7d-9dc0-d301-2dfd-5a6a21b6b033.png)

一番上のテキストボックスにタスクを入力し、右にある「add!」ボタンを押すことでタスクを追加できます。（Create）
その下には、既にデータベースに保存されているタスクが表示されます。（Read）
これらのタスクは、チェックボックスやテキストボックスの値を書き換えることで、自動的に更新されます。（Update）
また、タスク右の「delete」ボタンを押すことで、タスクを削除できます。（Delete）

※テキストボックスの値は、Enterキーを押すかフォーカスが外れたときに入力が確定します。

# 主な参考サイト

基本的には以下のサイトを参考にしていますが、一部自分なりにアレンジしています。

- [DockerでNode.jsアプリケーションを開発する (1) Express.jsをコンテナ内で動かす - Ishida-IT LLC](https://ishida-it.com/blog/post/2019-11-21-docker-nodejs/)
- [DockerでNode.jsアプリケーションを開発する (3) MySQL用コンテナを追加 - Ishida-IT LLC](https://ishida-it.com/blog/post/2019-11-23-docker-nodejs-mysql/)
- [Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる - Qiita]
(https://qiita.com/yoshiplum/items/129e7ad1ffc3a02b9eb2)

# 使用したソフトウェア等

DockerホストのPCはWindows10を使用しました。WSL2でUbuntuを実行し、Ubuntu上で作業を行いました。

- Windows10 バージョン 2004
- Ubuntu 20.04.2 LTS (Focal Fossa)
- Docker 20.10.2
- Node.js 14.16.0
- express 4.16.1
- sequelize 6.5.0
- sequelize-cli 6.2.0
- VueCLI 4.5.0
- Vue.js 3.0.0
- MySQL 8.0.23

# システムの全体構成

システムは、3つのコンテナで構成されています。

- `db`コンテナ（MySQLイメージを使用）
    - ToDoリストアプリのデータを保管するデータベースサーバーです。
- `vue`コンテナ（Node.jsイメージを使用）
    - ユーザーからのリクエストを受けるサーバーです。今回はフロントエンドにVue.jsを使うため、Vue CLIをインストールします。
- `api`コンテナ（Node.jsイメージを使用）
    - MySQLサーバーを操作するAPIを提供するサーバーです。`vue`コンテナは本サーバーが提供するAPIを利用してデータベースを操作します。Sequelizeをインストールします。

![Docker①.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/beb427bf-0d9a-66a9-8a7a-7d01478eec08.jpeg)

![Docker②.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/4c0cfdcd-ed38-438e-7017-caf9ee73e2f9.jpeg)

#プロジェクトのディレクトリを作成

ディレクトリの構成は以下のようになります。

- todo_app
    - docker-compose.yml
    - .env
    - api
        - （expressの雛型ファイル群が入る）
    - vue
        - （Vueの雛型ファイル群が入る）
    - db
        - logs
            - mysql-error.log
            - mysql-query.log
            - mysql-slow.log
        - conf
            - my.cnf

以下のコマンドを適当なディレクトリで実行してください。

```shell
mkdir -p todo_app/api
mkdir -p todo_app/vue
mkdir -p todo_app/db/logs
mkdir -p todo_app/db/conf
touch todo_app/docker-compose.yml
touch todo_app/.env
touch todo_app/db/logs/mysql-error.log
touch todo_app/db/logs/mysql-query.log
touch todo_app/db/logs/mysql-slow.log
touch todo_app/db/conf/my.cnf
```

# docker-compose.ymlファイルを編集

`todo_app/docker-compose.yml`ファイルを以下のように編集します。

```yml:todo_app/docker-compose.yml
version: '3'
services:
  db:
    # 起動するイメージを指定
    image: mysql:8.0.23

    # 環境変数を設定
    environment:
      - MYSQL_ROOT_HOST=${DB_ROOT_HOST}
      - MYSQL_DATABASE=${DB_NAME}
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_ROOT_PASSWORD=${DB_PASS}
      - TZ=${TZ}

    # ホスト側のポート:コンテナのポート
    ports:
      - '3306:3306'

    # ボリュームバインド
    volumes:
      - ./db/conf:/etc/mysql/conf.d/:ro
      - mysqldata:/var/lib/mysql
      - ./db/logs:/var/log/mysql

    #使用するネットワーク
    networks:
      - backend

  api:
    image: node:14.16.0-buster
    environment:
      - MYSQL_SERVER=db
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_DATABASE=${DB_NAME}
      - TZ=${TZ}
      - CHOKIDAR_USEPOLLING=true

    #コンテナを起動させ続けるよう設定
    tty: true

    ports:
      - '3000:3000'

    # ソースコードを格納するフォルダをマウント
    #（ホスト側の./apiをコンテナの/appにマウント）
    volumes:
      - ./api:/app

    # 起動時のカレントフォルダを指定
    working_dir: /app

    # 起動後に実行するコマンドを指定
    command: npm run dev

    networks:
      - backend

    #依存関係（apiコンテナより先にdbコンテナが起動するように設定）
    depends_on:
      - db

  vue:
    image: node:14.16.0-buster
    environment:
      - CHOKIDAR_USEPOLLING=true
    tty: true
    ports:
      - '8080:8080'
    volumes:
      - ./vue:/app
    working_dir: /app
    command: npm run serve
    networks:
      - backend
    depends_on:
      - api

networks:
  backend:

volumes:
  mysqldata:
```

`db`、`api`、`vue`の3つのサービスを定義しています。

###イメージ`image:`
イメージはMySQLとNode.jsの公式イメージをそのまま使用しています。（今回はDockerfileを使いません。）Node.jsイメージは`node:14.16.0-buster`を使用しています。なお、`buster`というのはDebianのVer.10のことです。ちなみにDebianのバージョン名は映画「トイ・ストーリー」の登場キャラクターが元になっているそうです。バージョン`14.16.0`は記事執筆時点での最新のLTS版を使用しています。

###環境変数`environment:`
`db`コンテナと`api`コンテナには、MySQLを使用（MySQLに接続）するための環境変数を設定していますが、後述する`.env`ファイルで定義したものを取り込むようにしています。直接docker-compose.ymlファイルに記述しなかったのは、データベースの接続情報をソースに含めるのはよろしくないからです。Githubに公開するときは`.gitignore`に`.env`ファイルを追加し、ソース管理から除外しましょう。
`api`コンテナと`vue`コンテナの環境変数には`- CHOKIDAR_USEPOLLING=true`を設定しています。これを書かないと、apiコンテナに入れるソースコードの変更を検知して自動でアプリを再起動するライブラリ「nodemon」が正常に動作しません。一応`vue`コンテナにも書きました。
参考：[Docker 環境で nodemon が watch してくれない問題と対処方法 - Qiita](https://qiita.com/hoto17296/items/90f04fee8b70128ede19)

###ポート`ports:`
ユーザーからリクエストを受ける`vue`コンテナの8080番ポートと、ホスト側の8080番ポートを対応させています。
残りの2つのコンテナはコンテナ間のみで通信できればいいので、Dockerホストにマッピングする必要はないですが、`api`コンテナもホストから動作を確認したいので設定しています。`api`コンテナの3000番ポートと、ホスト側の3000番ポートを対応させています。（なお、`db`コンテナも対応付けていますが、これは意味ないです。）

###ボリューム`volumes:`
`db`コンテナでは、ホスト側の`todo_app/db/conf`ディレクトリをコンテナの`/etc/mysql/conf.d/`ディレクトリにバインドマウントして、MySQLのデフォルト設定を記述した`my.cnf`ファイル（後述）を上書きしています。
また、MySQLのデータはコンテナが削除されてもデータが消えないように、`mysqldata`というボリュームを作成し、コンテナの`/var/lib/mysql`ディレクトリにボリュームマウントしています。（Windows環境ではバインドマウントだと上手くいかないそうです。）
さらに、MySQLのログデータを格納しているディレクトリもバインドマウントすることで、ホストからログを確認できるようにしています。
`api`コンテナでは、ホスト側の`todo_app/api`をapiコンテナの`/app`にバインドマウントすることで、アプリケーションのファイル群をホスト側から編集可能にしています。
`vue`コンテナでも同様です。（ホスト側の`todo_app/vue`をvueコンテナの`/app`にバインドマウント）

###コマンド`command:`
`api`コンテナおよび`vue`コンテナでは、起動時に実行するコマンドを設定しています。しかし、現時点ではこれらのコマンドを動作させるために必要なライブラリがインストールされていないため、コンテナを起動してもすぐに終了してしまいます。

###ネットワーク`networks:`
コンテナ名でコンテナ間の通信を行うために、`backend`という名前のDockerネットワークを作り、`db`コンテナ、`api`コンテナ、`vue`コンテナで設定しています。

###依存関係`depends_on:`
依存関係を設定しました。`db`→`api`→`vue`コンテナの順で起動します。停止するときは逆順になります。`vue`コンテナ（Todoリストアプリ）は`api`コンテナの提供するAPIを使用しないとデータのCRUD（作成・取得・更新・削除）ができません。つまり、`vue`コンテナは`api`コンテナに依存していると言えます。その`api`コンテナも、データを`db`コンテナに取りにいかなければなりません。`api`コンテナは`db`コンテナに依存していると言えます。


# my.cnfの編集

`todo_app/db/conf/my.cnf`を以下のように編集します。

```conf:todo_app/db/conf/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4
collation-server = utf8mb4_bin

# タイムゾーンの設定
default-time-zone = SYSTEM
log_timestamps = SYSTEM

# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password

# エラーログの設定
log-error = /var/log/mysql/mysql-error.log

# スロークエリログの設定
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0

# 実行ログの設定
general_log = 1
general_log_file = /var/log/mysql/mysql-query.log

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4
```

ここでは、文字コードの設定や、ログの出力先設定などを行っています。
また、デフォルトの認証プラグインを変更しています。

なお、DockerホストがWindowsの場合、`todo_app/db/conf/my.cnf`は読み取り専用にしておきます。
参考：[[Docker+Windows]mysqlのdockerイメージがmy.cnfのマウントのエラーで起動しない時の対処法 - Qiita](https://qiita.com/rabbitbeef/items/14433a2c0a6f85c3b476)

![my.cnfを読み取り専用にする①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/7a1d2fd6-3555-5db7-1575-76c541fb3db0.png)

![my.cnfを読み取り専用にする②.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/61353a96-4e12-53bc-010a-c6b83be537f5.png)

# .envファイルの編集

`todo.app/.env`ファイルを以下のように編集します。
docker-compose.ymlでMySQLの環境変数を設定しますが、このファイルから具体的な値を取得しています。
参考：[docker-composeのenv_fileと.envファイルの違い - Qiita](https://qiita.com/SolKul/items/989727aeeafcae28ecf7)

```env:todo_app/.env
DB_ROOT_HOST=%
DB_NAME=todo
DB_USER=username
DB_PASS=mypassword
TZ=Asia/Tokyo
```

# apiコンテナを起動してコンテナ内でExpress.jsアプリケーションを作成する

`api`コンテナを起動して、express.jsアプリケーションの雛型を作成していきます。
docker-compose.ymlが置いてあるディレクトリ（`todo_app/`）で次のコマンドを実行します。

```shell
#docker-compose.ymlが置いてあるディレクトリに移動
cd todo_app

#apiコンテナを一時的に起動
$ docker-compose run --rm --no-deps api /bin/bash
```

このコマンドは、`api`コンテナを実行（`run`）し、シェルを起動する（`/bin/bash`）という意味になります。
`--rm`は、実行後に自動でコンテナを削除するオプションです。
`--no-deps`は、リンクしたサービスを起動しないようにするオプションです。docker-compose.ymlでコンテナの依存関係を定義したため、本来ならば`api`コンテナが起動する前に`db`コンテナが立ち上がるはずですが、このオプションを設定することで`api`コンテナのみ起動するようにしています。

lsコマンドで`/app`ディレクトリ内に何もないことを確認しておきましょう。

```shell
==== ここからapiコンテナ内 ====
# lsコマンドで、/appディレクトリ（=ホスト側のtodo_app/api）内にまだ何もないことを確認
ls -al
total 4
drwxrwxrwx 1 node node 4096 Feb 15 11:48 .
drwxr-xr-x 1 root root 4096 Feb 15 11:46 ..
```

私の環境ではnpmのバージョンを上げておかないとライブラリのインストールが失敗してしまうことがあったので、npmをアップデートしておきます。（今後も何度かこの操作を行うのでDockerfileに書いておくべきだったかもしれません…。）

```shell
==== apiコンテナ内 ====
# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install -g npm

# npmがアップデートされたことを確認
npm -v
7.6.1
```

`npm install`でexpress-generatorをインストールして実行します。
（なぜか`npx express-generator`では上手くいきませんでした。npmのバージョンを上げなければうまくいくのですが…）

```shell
==== apiコンテナ内 ====
# express-generatorをインストールして実行
npm install -g express-generator
express .
```

nodemonをインストール。ソースコードを変更したときに、自動でサーバーを再起動してくれる便利なツールです。開発環境でのみ使用するので`-D`（`--save-dev`）オプションを付けておきます。

```shell
==== apiコンテナ内 ====
# nodemonをインストール
npm install -D nodemon
```

exitでコンテナを抜けます。

```shell
==== apiコンテナ内 ====
# exitでコンテナを抜ける
exit
==== ここまでapiコンテナ内 ====
```

`todo_app/api/package.json`を編集して、`"scripts"`に`"dev"`を追加します。

```json:todo_app/api/package.json
{
    "name": "app",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "dev": "nodemon ./bin/www",
        "start": "node ./bin/www"
    },
    "dependencies": {
        "cookie-parser": "~1.4.4",
        "debug": "~2.6.9",
        "express": "~4.16.1",
        "http-errors": "~1.6.3",
        "jade": "~1.11.0",
        "morgan": "~1.9.1",
        "mysql2": "^2.2.5",
        "sequelize": "^6.5.0",
        "sequelize-cli": "^6.2.0"
    },
    "devDependencies": {
        "nodemon": "^2.0.7"
    }
}

```

以下のコマンドで3つのコンテナをまとめて起動します。

```
$ docker-compose up -d
```

起動後、apiコンテナは`npm run dev`が自動的に実行されます。（docker-compose.ymlで定義しました。）
[http://localhost:3000/](http://localhost:3000/)に接続して、Expressの初期画面が表示されることを確認します。

![Expressの初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/692ce579-37a5-6808-bd66-2dfdf338bec6.png)

ここで、`docker-compose ps`コマンドでコンテナの起動状況を確認しておきます。

```
$ docker-compose ps
```

vueコンテナだけ状態（`State`）が`Exit`になっています。（停止状態になっています。）

![vueコンテナだけExitになっている.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/c9eb1fa1-c95f-635b-b99e-6d112de3754c.png)

これは、vueコンテナを起動時に実行されるコマンド（`npm run serve`）の実行に失敗したためです。後ほどvueコンテナに入りVue CLIをインストールすることで、このコマンドを実行できるようになります。

# dbコンテナを起動して正常に設定されていることを確認

## 起動済みのdbコンテナに入り、MySQLにログインする。

以下のコマンドを実行し、dbコンテナのシェルを起動してMySQLにログインします。

```shell
$ docker-compose exec db /bin/bash

==== ここからdbコンテナ内 ====
mysql -u root -p
```

MySQLログイン時のパスワードは、`.env`ファイルで指定したものを入力してください。（今回は`mypassword`）

## MySQLの文字コードを確認

MySQLにログイン出来たら、念のため文字コードの設定を確認します。

```shell
==== dbコンテナ内 ====
mysql> show variables like 'char%';
```
![MySQL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/e34224af-8f09-615a-9d2c-4df6676526ce.png)

`todo_app/db/conf/my.cnf`で指定した通りの設定になっていればOKです。

## データベースを確認

データベースの状態を確認します。

```shell
==== dbコンテナ内 ====
mysql> show databases;
```

![データベースの確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/43e9a59a-acbf-00ca-c6e9-309fa93d511a.png)

環境変数で指定した`todo`というデータベースが存在しているのがわかります。
`todo`データベースの中身はどうなっているでしょうか？

```shell
==== dbコンテナ内 ====
mysql> use todo;
mysql> show tables;
```

![データベース切り替え&テーブル確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/d9dd334c-783c-e06c-7e70-bda33aef4c7f.png)

`todo`データベースの中身は空です。（テーブルは1つも存在しません。）

テーブルの作成は手作業でもできますが、今回はapiコンテナからSequelizeというライブラリのDBマイグレーション機能を使用して行います。
`quit`でMySQLを抜け、`exit`でdbコンテナも抜けてください。

```shell
==== dbコンテナ内 ====
mysql> quit
exit
==== ここまでdbコンテナ内 ====
```

# apiコンテナ内で、sequelizeとその依存パッケージをインストール

今度はapiサーバーを設定していきます。
apiコンテナのシェルを立ち上げ、npmのバージョンをアップデートしておきます。

```shell
$ docker-compose exec api /bin/bash
==== apiコンテナ内 ====
# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install -g npm

# npmがアップデートされたことを確認
npm -v
7.6.1
```

まず、APIコンテナ内でSequelizeとその依存パッケージをインストールします。

```shell
==== apiコンテナ内 ====
npm install mysql2 sequelize sequelize-cli
```

次に、sequelize-cliを使用してSequelizeの初期化を行います。

```shell
==== apiコンテナ内 ====
npx sequelize-cli init
```

これによって、apiフォルダ内に`config`、`migrations`、`models`、`seeders`の4つのディレクトリが作成されます。

次に、以下のコマンドでモデルクラスを生成します。

```shell
==== apiコンテナ内 ====
npx sequelize-cli model:generate --name Task --attributes name:string,done:boolean
```

上記の例では、string型の`name`と、boolean型の`done`という2つのプロパティを持つ、`Task`というモデルクラスを作成しています。

| カラム名 | 型      |説明|
|:-------:|:-------:|:-------:|
| name    | string  |タスク名|
| done    | boolean |タスクが終了したか否か|

実行すると、`todo_app/api/models`ディレクトリに、`task.js`ファイルが出来ているのがわかります。

```js:todo_app/api/models/task.js
'use strict';
const {
    Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
    class Task extends Model {
        /**
         * Helper method for defining associations.
         * This method is not a part of Sequelize lifecycle.
         * The `models/index` file will call this method automatically.
         */
        static associate(models) {
            // define association here
        }
    };
    Task.init({
        name: DataTypes.STRING,
        done: DataTypes.BOOLEAN
    }, {
        sequelize,
        modelName: 'Task',
    });
    return Task;
};
```

また、同時に`todo_app/api/migrations`ディレクトリに、`{日時}-create-task.js`というファイルも出来ています。

```js:todo_app/api/migrations/[yyyyMMddHHmmss]-create-task.js
'use strict';
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('Tasks', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                type: Sequelize.STRING
            },
            done: {
                type: Sequelize.BOOLEAN
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE
            },
            updatedAt: {
                allowNull: false,
                type: Sequelize.DATE
            }
        });
    },
    down: async (queryInterface, Sequelize) => {
        await queryInterface.dropTable('Tasks');
    }
};
```

この時点のフォルダ構成は次の通りです。

![フォルダ構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/ded5922a-be31-77de-1ba2-9eb15a9094b7.png)

# sequelize-cliでDBマイグレーションを実行

次は、DBマイグレーションを実行して`todo`データベースにテーブルを作成します。
しかし、その前にDB接続情報を正しく設定する必要があります。

先程`npx sequelize-cli init`でSequelizeの初期化を行った時に、`todo_app/api/config`ディレクトリに`config.json`というJSONファイルが出来ているはずですが、それを`config.js`にしてJavascriptファイルにします。
そして内容を次のように書き換えます。
これは、docker-compose.ymlで設定した環境変数の値をそのままDB接続情報として使うためです。

```js:todo_app/api/config/config.js
module.exports = {
    development: {
        username: process.env.MYSQL_USER,
        password: process.env.MYSQL_PASSWORD,
        database: process.env.MYSQL_DATABASE,
        host: process.env.MYSQL_SERVER,
        dialect: 'mysql',
    },
    test: {
        username: process.env.MYSQL_USER,
        password: process.env.MYSQL_PASSWORD,
        database: process.env.MYSQL_DATABASE,
        host: process.env.MYSQL_SERVER,
        dialect: 'mysql',
    },
    production: {
        username: process.env.MYSQL_USER,
        password: process.env.MYSQL_PASSWORD,
        database: process.env.MYSQL_DATABASE,
        host: process.env.MYSQL_SERVER,
        dialect: 'mysql',
    },
};
```

また、`todo_app/api/models/index.js`ファイルを開き、`config.json`となっている部分を`config.js`に変更します。

![index.jsの修正.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/a34d38c9-4c9d-b58b-6841-211de3044c4d.png)

この状態で、以下のコマンドを実行するとSequelizeによるDBマイグレーションが実行されます。
つまり、何もなかった`todo`データベースに`Tasks`テーブルが作成されます。

```shell
==== apiコンテナ内 ====
npx sequelize-cli db:migrate
```

最後に、`exit`でapiコンテナを抜けましょう。

```shell
==== apiコンテナ内 ====
exit
==== ここまでapiコンテナ内 ====
```

# Tasksテーブルが作成されたことを確認

`todo`データベースに`Tasks`テーブルが作成されたことを確認しておきます。
以下のコマンドでdbコンテナのシェルを実行してください。

```shell
docker-compose exec db /bin/bash
```

先程と同様に、MySQLにログイン後、`todo`データベースに切り替え、テーブルを見てみます。

```shell
==== ここからdbコンテナ内 ====
mysql -u root -p
mysql> use todo
mysql> show tables;
```

![Tasksテーブルの確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/88eab024-d4d1-f341-87ed-e6ef92674967.png)

結果、`Tasks`テーブルが出来ていることが確認できました。なお、初回はマイグレーション管理用に`SequelizeMeta`テーブルも作られます。

ではMySQLとdbコンテナを抜け、ホストに戻ります。

```shell
==== dbコンテナ内 ====
mysql> quit
exit
==== ここまでdbコンテナ内 ====
```

# vueコンテナの準備

ここで、ユーザーからのリクエストを受け取るvueコンテナの準備をしていきます。以前も触れましたが、vueコンテナは起動時に実行されるはずの`npm run serve`コマンドが実行できなかったため、停止しています。（`docker-compose ps`でもう一度確認してみましょう。）
ここでは、Vue CLIをインストールし、`npm run serve`コマンドを使えるようにしていきます。
vueコンテナは停止しているため、`docker-compose exec`は使えません。ここでは、`docker-compose run`を使ってvueコンテナを一時的に起動します。

```shell
docker-compose run --rm --no-deps vue /bin/bash
```

例によってnpmのバージョンをアップデートしておきます。

```shell
==== vueコンテナ内 ====
# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install -g npm

# npmがアップデートされたことを確認
npm -v
7.6.1
```

npmのアップデート後、Vue CLIをインストールします。

```shell
==== vueコンテナ内 ====
npm install -g @vue/cli
```

インストール完了後、現在のディレクトリ`.`を対象に`vue create`を行います。

```shell
==== vueコンテナ内 ====
vue create .
```

以下の画像を参考に進めていきます。

![vue create①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/4bdcd132-57f5-9a88-b23a-2082d9237688.png)

↑デフォルトで参照しているレジストリでは接続が遅く、「こっち（`https://registry.npm.taobao.org`）の方が早いからこっちを使おうよ！」と聞いているようですが、よくわからないのでNo（`n`）を選択しました。ちなみにtaobaoというのは中国のサーバーらしいです。

![vue create②.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/dbb5440f-eb45-c55e-e20e-662b50f3b66c.png)

↑カレントディレクトリに作っていいのか聞いています。Yes（`Y`）で大丈夫です。

![vue create③.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/4ba38ecd-20d8-4ef2-564b-ed5f9d250c6e.png)

↑プリセットを選択します。`Default (Vue 3 Preview) ([Vue 3] babel, eslint)`を選びました。

![vue create④.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/e9aa4990-5dea-3d14-b4bf-1a0dd6a047d0.png)

↑パッケージマネージャーを選択します。`NPM`を選びました。
その後、待っていれば`vue create`が終わります。（私の環境だと結構時間がかかりました…）

次に、apiコンテナ（APIサーバー）からデータの取得や更新を行うために、axiosをインストールします。

```shell
==== vueコンテナ内 ====
npm install axios
```

インストールが完了したら、`exit`でvueコンテナを抜けます。

```shell
==== vueコンテナ内 ====
exit
==== ここまでvueコンテナ内 ====
```

そして、`docker-compose down`で全てのコンテナを停止&削除した後、`docker-compose up -d`で再び起動し直します。

```shell
docker-compose down
docker-compose up -d
```

ここで、`docker-compose ps`で各コンテナのステータスを確認してみると、今度はvueコンテナも稼働状態になっていることがわかります。Vue CLiをインストールしたことで、`npm run serve`コマンドが実行できるようになりました。

```shell
docker-compose ps
```

![vueコンテナもUpになっている.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/cf1cbc38-7ee7-e706-5d8d-d5d65cf67f15.png)

[http://localhost:8080/](http://localhost:8080/)に接続し、Vue.jsの初期画面が表示されることを確認します。

![Vue.jsの初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/1f336e59-b3b0-3e2a-c541-9d71a9c0e3e6.png)


# APIの実装

ここからは、apiコンテナ内のプログラムを編集し、ToDoリストアプリのCRUDのAPIを実装していきます。
まず、`todo_app/api/routes/index.js`に2行追加します。
`/task`ルートの処理を`taskRoutes.js`に投げるようにします。

```js:todo_app/api/routes/index.js
var express = require('express');
var router = express.Router();

//この2行を追加する
const taskRoutes = require("./taskRoutes");
router.use("/task", taskRoutes);

/* GET home page. */
router.get('/', function (req, res, next) {
    res.render('index', { title: 'Express' });
});

module.exports = router;
```

次に、`todo_app/api/routes/`に、ファイル`taskRoutes.js`を作成して、以下のように編集します。

```js:todo_app/api/routes/taskRoutes.js
"use strict";

const router = require("express").Router(),
    taskController = require("../controllers/taskController");

router.get("/", taskController.read);
router.post("/", taskController.create);
router.put("/:id", taskController.update);
router.delete("/:id", taskController.delete);

module.exports = router;
```

ここでは、4つのhttpメソッド（get、post、put、delete）毎に違う処理を割り当てています。
処理の内容は、`todo_app/api/controllers/taskController.js`内に記述していきます。

`todo_app/api/`に、`controllers`という名前のディレクトリを作成し、その中に`taskController.js`を作成します。
`todo_app/api/controllers/taskController.js`は、以下のように編集します。

```js:todo_app/api/controllers/taskController.js
"use strict";

const db = require("../models/index");

module.exports = {
    read: async(req, res, next) => {
        try{
            const result = await db.Task.findAll();
            res.send(result);
        }catch(err){
            res.status(500).send(err);
        }
    },
    create: async(req, res, next) => {
        try{
            const result = await db.Task.create({
                name: req.body.name,
                done: false
            });
            res.send(result);
        }catch(err){
            res.status(500).send(err);
        }
    },
    update: async(req, res, next) => {
        try{
            const result = await db.Task.update(
                {
                    name: req.body.name,
                    done: req.body.done
                },
                {
                    where: {
                        id: req.params.id
                    }
                }
            );
            res.send(result);
        }catch(err){
            res.status(500).send(err);
        }
    },
    delete: async(req, res, next) => {
        try{
            const result = await db.Task.destroy({
                where: {
                    id: req.params.id
                }
            });
            res.send({
                result: result
            });
        }catch(err){
            res.status(500).send(err);
        }
    }
}
```

これで、APIコンテナの実装は完了です。

# Vue.jsの実装

最後に、フロントエンドの実装を行います。
デフォルトで存在する`todo_app/vue/src/components/HelloWorld.vue`というコンポーネントを以下のように書き換えます。

```vue:todo_app/vue/src/components/HelloWorld.vue
<template>
  <div class="hello">
    <form>
      <input type="text" style="display: none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" @click="taskCreate" />
    </form>
    <table align="center" border="0">
      <tr>
        <th>done</th>
        <th>task</th>
        <th>delete</th>
      </tr>
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input
            type="checkbox"
            v-model="task.done"
            @change="taskUpdate(task.id, task.name, task.done)"
          />
        </td>
        <td>
          <input
            type="text"
            v-model="task.name"
            @change="taskUpdate(task.id, task.name, task.done)"
          />
        </td>
        <td>
          <input type="button" value="delete" @click="taskDelete(task.id, index)" />
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
    currentTask: "",
  }),
  created: async function () {
    try {
      const result = await axios.get("/task/");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  },
  methods: {
    taskCreate: async function () {
      try {
        const result = await axios.post("/task/", {
          name: this.currentTask,
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskDelete: async function (id, index) {
      try {
        await axios.delete("/task/" + id);
        this.currentTask = "";
        this.tasks.splice(index, 1);
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskUpdate: async function (id, val, done) {
      try {
        await axios.put("/task/" + id, {
          name: val,
          done: done,
        });
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
.table {
  height: 100%;
  text-align: center;
}
</style>
```

次に、`todo_app/vue/`にファイル`vue.config.js`を作成し、以下のように編集します。
参考：[Vue.jsとAPIサーバとのaxiosでCORSに引っかかった時のProxyを使った回避方法 - Qiita](https://qiita.com/Ryoma0413/items/c41d10d2e6e2a420c3cf)

```js:todo_app/vue/vue.config.js
module.exports = {
    devServer: {
        proxy: 'http://api:3000'
    }
};
```

編集し終わったら、ここで各コンテナを再起動しましょう。

```shell
docker-compose down
docker-compose up -d
```

これで、ToDoリストアプリが完成しているはずです。[http://localhost:8080/](http://localhost:8080/)にアクセスして操作してみましょう。

![ToDoリストアプリの完成形.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667062/dd95edb4-f240-eac4-9ef8-8cf68bbe58fa.png)

出来ました！

# 参考書籍

- [さわって学ぶクラウドインフラ　docker基礎からのコンテナ構築](https://www.nikkeibp.co.jp/atclpubmkt/book/20/279230/)
    - わかりやすい解説で理解しやすかったです。

# 主な参考サイト

- [DockerでNode.jsアプリケーションを開発する (1) Express.jsをコンテナ内で動かす - Ishida-IT LLC](https://ishida-it.com/blog/post/2019-11-21-docker-nodejs/)
- [DockerでNode.jsアプリケーションを開発する (3) MySQL用コンテナを追加 - Ishida-IT LLC](https://ishida-it.com/blog/post/2019-11-23-docker-nodejs-mysql/)
- [Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる - Qiita]
(https://qiita.com/yoshiplum/items/129e7ad1ffc3a02b9eb2)