DockerでNode.jsアプリケーションを開発する (3) MySQL用コンテナを追加
Posted: 2019-11-23
1. docker-compose.ymlファイルを編集
docker-compose.ymlファイルにMySQLコンテナに関する記述を追加します。
今回のサンプルではMySQL5.7の公式イメージを使うことにします。
docker-compose.yml
version: '3'
services:
mysql:
image: mysql:5.7
env_file: ./mysql/mysql.env
environment:
- TZ=Asia/Tokyo
ports:
- '3306:3306'
volumes:
- ./mysql/conf:/etc/mysql/conf.d/:ro
- mysqldata:/var/lib/mysql
networks:
- backend
app:
image: node:12
env_file: ./app.env
environment:
- TZ=Asia/Tokyo
- DEBUG=app:*
tty: true
ports:
- '3000:3000'
volumes:
- ./src:/app
working_dir: /app
command: npm run dev
networks:
- backend
depends_on:
- mysql
networks:
backend:
volumes:
mysqldata:
「mysql」と「app」の2つのサービスを定義しています。
mysqlサービスの設定では、「volumes:」でホスト側のmysql/confフォルダをコンテナの/etc/mysql/conf.d/ディレクトリにマウントしてMySQLのデフォルト設定を上書きしています。
また、「mysqldata」というボリュームをコンテナの/var/lib/mysql/ディレクトリにマウントすることで、コンテナが削除されてもデータが消えずに保持されるようにしています(ホスト側のフォルダをそのままmysqlのデータ保存先としてマウントするとWindows環境で上手く動かなかったので、Docker側で管理されたボリュームをマウントするように修正しました)。
「mysql」「app」の両コンテナとも「environment:」で「TZ=Asia/Tokyo」としてタイムゾーンを日本時間に合わせています。もしUTCで良い場合はこの設定は不要です。
またその他の環境変数については「env_file:」の指定で別ファイルに定義したものを取り込むようにしています。もちろんdocker-compose.yml内に全てを書いても構わないのですが、一般的にはデータベースの接続情報などをソースコントロールに含めない方が良いと思いますのでこのサンプルでも「.gitignore」に「*.env」を追加してソース管理から除外するようにしました。
mysql/conf/フォルダにmy.cnfファイルを作成します。
mysql/conf/my.cnf
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqldump]
default-character-set=utf8mb4
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
lower_case_table_names=1
# Enable access from the host machine.
bind-address=0.0.0.0
この例では日本語などのマルチバイト文字列とEmojiが使えるように設定しています。
mysqlフォルダにmysql.envファイルを作成します。
mysql/mysql.env
MYSQL_ROOT_HOST=%
MYSQL_ROOT_PASSWORD=(ルートパスワード)
MYSQL_USER=(ユーザー名)
MYSQL_PASSWORD=(パスワード)
MYSQL_DATABASE=todo
これはMySQLのコンテナに与える環境変数の設定になります。データベース名は「todo」としておきました。
ルートフォルダにapp.envファイルを作成します。
app.env
MYSQL_SERVER=mysql
MYSQL_USER=(ユーザー名)
MYSQL_PASSWORD=(パスワード)
MYSQL_DATABASE=todo
これはNode.jsのアプリケーションが動くコンテナに与える環境変数の設定になります。アプリケーション内でこの環境変数の値を見てデータベースに接続します。
2. MySQLコンテナを動かす
> docker-compose up -d
上のコマンドでmysqlとappの2つのコンテナが起動するはずです。
docker-compose psで確認しておきましょう。
> docker-compose ps
もし起動しない場合は、docker-compose.ymlの内容を再度確認してください。
3. MySQLにログイン
「docker-compose exec」を使ってMySQLサーバにログインします。
> docker-compose exec mysql mysql -uroot -p
コンテナがすでに動いているので、この場合は「run」ではなく「exec」を使って動いているコンテナ内でコマンドを実行します。
「mysql」が2回続いていますが、1回目はdocker-composeが認識するサービス名、2回目はコンテナ内で実行するコマンド名としての「mysql」です。
ログイン時のパスワードは、mysql/mysql.envで指定したパスワードを入力します。
4. MySQLの文字コード設定を確認
MySQLにログイン出来たら、念のため文字コードの設定を確認しておきましょう。
mysql> show variables like 'char%';
「mysql/conf/my.cnf」ファイルで指定した通りの設定が反映されていればOKです。
5. データベースを確認
データベースの状態も確認しておきます。
mysql> show databases;
環境変数で指定した「todo」というデータベースが存在しているのが分かります。
mysql> use todo;
mysql> show tables;
todoデータベースの中身は現時点では何もありません。
テーブルの作成と開発用初期データの挿入はもちろん手作業でも構わないのですが、SequelizeというライブラリのDBマイグレーション機能を利用するのが便利です。
6. Express.jsアプリケーションにSequelizeを導入
まずはアプリケーション用コンテナ内でSequelizeとその依存パッケージをインストールします。
> docker-compose run --rm app npm i mysql2 sequelize sequelize-cli
次に、sequelize-cliを使ってSequelizeの初期化を行います。
> docker-compose run --rm app npx sequelize-cli init
これによって、srcフォルダ内に
- config
- migrations
- models
- seeders
の4つのフォルダが作成されます。
次に、「sequelize-cli model:generate」を使って最初のモデルクラスを生成します。
> docker-compose run --rm app npx sequelize-cli model:generate --name Task --attributes task:string,done:boolean
上の例では「task」と「done」という2つのプロパティを持つ「Task」というモデルクラスを生成しています。
modelsフォルダに「task.js」ファイルが出来ているのが分かります。
models/task.js
'use strict';
module.exports = (sequelize, DataTypes) => {
const Task = sequelize.define(
'Task',
{
task: DataTypes.STRING,
done: DataTypes.BOOLEAN,
},
{}
);
Task.associate = function(models) {
// associations can be defined here
};
return Task;
};
また同時にmigrationsフォルダに「yyyyMMddHHmmss-create-task.js」というファイルも出来ています。
migrations/[yyyyMMddHHmmss]-create-task.js
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Tasks', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
task: {
type: Sequelize.STRING,
},
done: {
type: Sequelize.BOOLEAN,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Tasks');
},
};
この時点でのフォルダ構成は下の画像のようになります。
7. sequelize-cliでDBマイグレーションを実行
さて、次はDBマイグレーションを実行してtodoデータベースにテーブルを作成したいところですが、その前にDB接続情報を正しくセットアップする必要があります。
自動で生成された状態だとconfigフォルダに「config.json」というファイルが出来ていると思いますが、それを「config.js」にリネームして、内容を下のように書き換えます。
これは、docker-composeで設定した環境変数の値をそのままDB接続情報として使うためです。
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',
},
};
また、modelsフォルダのindex.jsファイルを開き、'config.json'となっている部分を'config.js'に変更しておきます。
この状態で、下のコマンドを実行すると、SequelizeによるDBマイグレーションが実行されます。
今回のサンプルの場合は、何も無かったtodoデータベースにtasksテーブルが作成されます。
> docker-compose run --rm app npx sequelize-cli db:migrate
8. sequelize-cliで初期データを挿入
アプリケーションの開発中やテスト中はデータベースの内容を初期化したりデータを任意の状態に戻したりする場合がありますが、そのようなときにはSequelizeのシード機能が使えます。
seedersフォルダ内に「yyyyMMdd-tasks.js」という名前のファイルを作って下の内容で保存します。
'use strict';
const db = require('../models/');
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert(
'tasks',
[
{
task: 'Write the blog article',
done: false,
createdAt: new Date(),
updatedAt: new Date(),
},
{
task: 'Purchase new laptop PC',
done: false,
createdAt: new Date(),
updatedAt: new Date(),
},
{
task: 'Go to swim',
done: false,
createdAt: new Date(),
updatedAt: new Date(),
},
{
task: 'Order a pizza',
done: true,
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('Users', null, {});
},
};
次のコマンドを実行すると、tasksテーブルに4件のレコードが挿入されます。
> docker-compose run --rm app npx sequelize-cli db:seed:all
9. Express.jsアプリケーションでデータの一覧を表示
データベースに初期データが投入出来たら、次にアプリケーションのトップ画面にタスク一覧を表示しましょう。
const db = require('../models');
とすることで、db.TaskでTaskモデルを参照することが出来るようになっています。
あとは、
const tasks = await db.Task.findAll();
でTaskの一覧を取得することが出来ます。
そのTask一覧をViewに渡して、router.get('/')ハンドラのやることは終わりです。
routes/index.js
const express = require('express');
const router = express.Router();
const db = require('../models');
router.get('/', async function(req, res) {
const tasks = await db.Task.findAll();
res.render('index', { title: 'Docker-Node.js', tasks });
});
views/index.jadeファイルでは、渡されてきたtasksに対して
each t in tasks
という書式でループしてリストを表示しています。
views/index.jade
extends layout
block content
h1= title
h2 Tasks
ul.tasks
each t in tasks
li
form(action="update", method="post")
input(type='hidden' name='id' value=t.id)
input(
type='checkbox'
name='done'
checked=t.done
onclick="this.closest('form').submit();"
)
span.task= t.task
form(action="delete", method="post")
input(type='hidden' name='id' value=t.id)
button.delete X
div.newtask
form(action="create", method="post")
input(type='text' name='task')
button(type='submit') Add
このViewでは、Taskの一覧を表示する他に、Create/Update/Deleteが出来るようになっています。
それぞれのUI要素をFormタグで囲んで、ボタンが押されたら
- /create
- /update
- /delete
というURLにデータをPOSTするようになっています。
routes/index.jsの方ではそれぞれのURLに対応したルートハンドラを定義してTaskの挿入・更新・削除の処理を行っています。
routes/index.js (続き)
router.post('/create', async function(req, res) {
const newTask = db.Task.build({
task: req.body.task,
done: false
});
await newTask.save();
res.redirect('/');
});
router.post('/update', async function(req, res) {
const task = await db.Task.findByPk(req.body.id);
if (task) {
task.done = !!(req.body.done);
await task.save();
}
res.redirect('/');
});
router.post('/delete', async function(req, res) {
const task = await db.Task.findByPk(req.body.id);
if (task) {
await task.destroy();
}
res.redirect('/');
});
module.exports = router;
今回はサンプルなのでエラー処理などを省略してしまいましたが、本格的にSequelizeを使ったアプリケーションの作成方法については、また別のブログ記事として詳しく書きたいと思います。
最後にスタイルシートを編集して見た目を整えます。
最終的に出来上がったのは、下の画像のようなTodoリストアプリケーションでした。
ソースコード
ここまでの全ソースコードは、下記から参照可能です。
https://github.com/ishidait/docker-nodejs/tree/blog-3次回
次回はこのアプリケーションをHerokuにデプロイして公開するまでの流れを見てみたいと思います。