見出し画像

【Node】DockerでNext.js+MySQL

ReactやVue.jsなど、Node.jsアプリケーションを試す環境が欲しくなった。まずは、Next.js+MySQLな環境をDockerで作成し、DBを使うアプリケーションを作成してみる。

GitHubリポジトリはこちら


ディレクトリ構成

プロジェクトルート
├── app-next
│   └── アプリケーション構成ファイル
├── mysql
│   ├── conf.d
│   │   └── my.cnf
│   └── init.d
│       └── データベースの初期化SQL
├── .env
├── .gitignore
├── docker-compose.yml
└── Dockerfile

プロジェクトルートに、app-nextやらapp-react、app-vueなどのNode.jsアプリケーションをぶら下げていくつもり。

環境構築手順

1. 環境変数ファイル作成

DBの情報を持たせた.envファイルを作成する。環境依存ファイルをGitの無視リストに登録するのを忘れずに。

MYSQL_HOST=localhost
MYSQL_ROOT_PASSWORD=パスワード
MYSQL_DATABASE=DB名
MYSQL_USER=ユーザー名
MYSQL_PASSWORD=パスワード

2. MySQL設定ファイルの作成

my.cnfにタイムゾーンと文字コードを定義しておき、後でDBコンテナにマウントする。

[mysqld]
default-time-zone = 'Asia/Tokyo'
character-set-server = utf8mb4
collation-server = utf8mb4_bin
default-authentication-plugin = mysql_native_password

[client]
default-character-set = utf8mb4

MySQL8.04以降は、パスワード認証のハッシュ方式が変わったようだ。後で使用するNode.jsのパッケージが新しい方に対応しておらず、認証エラーが発生するので旧来のハッシュ方式を使うように定義しておく。

3. DB初期化SQLの作成

テーブルを作成してデータを投入するSQLを作成しておく。このような簡単なテーブルで今回は行ってみよう。

書籍と著者のテーブル

4. Dockerfileの作成

ベースイメージはNode.js。

FROM node:latest

# 作業ディレクトリ
WORKDIR /var/www

# ロケールのインストールと設定
RUN apt updateRUN apt -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

# Vimインストール
RUN apt install -y vim

# create-next-appをインストール
RUN npm install -g npm@latest && npm install create-next-app

Webサーバー(入れてないけど)の公開ディレクトリ以下にアプリケーションを作っていくイメージで作業ディレクトリを設定。

最低限必要なものをインストールしたら、Next.js(のアプリケーションを作成するためのパッケージ)をインストールする。

5. docker-compose.ymlの作成

Node.jsとMySQLと、それぞれコンテナで構成する。

version: '3'

services:
  # Node.js
  node:
    build: .
    container_name: node
    tty: true
    ports:
      - 3000:3000
    # プロジェクトディレクトリにマウントする
    volumes:
      - .:/var/www
    # ネットワーク
    networks:
      - default

  # データベース
  db:
    image: mysql:latest
    container_name: mysql
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      # 設定ファイルディレクトリにマウントする
      - ./mysql/conf.d:/etc/mysql/conf.d
      # DB初期化ディレクトリにマウントする
      - ./mysql/init.d:/docker-entrypoint-initdb.d
    # ネットワーク
    networks:
      - default

# ネットワーク
networks:
  default:
    driver: bridge

Nodeコンテナ
Webサーバーの公開ディレクトリをプロジェクトルートにマウントする。作成したアプリケーションがここに生成される。

DBコンテナ
MySQLへの接続情報は環境変数(.env)より取得する。ユーザーはビルド時に作成してくれる。設定ファイルと初期化のエントリポイントをローカルのディレクトリにマウントする。

ネットワーク
NodeコンテナとDBコンテナを同じブリッジネットワークに載せる。

6. コンテナビルド

Dockerイメージを作成する。

# コンテナビルド
$ docker-compose build

# イメージの確認
$ docker image ls -a
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
node-node    latest    fd515a4c08ce   42 hours ago   1.05GB

7. アプリケーションの作成

Next.jsのアプリケーション雛形作成コマンドをNodeコンテナで実行する。rmオプションは実行後にコンテナをいったん削除しなさいの意。

$ docker-compose run --rm node npx create-next-app app-next

Next.jsの初期化で訊かれる問答のうち、App Routerを使うかどうかについては「Yes」にする。

実行後、ローカルのプロジェクトディレクトリにアプリケーション雛形が作成されているはず。

8. コンテナ起動

# コンテナ起動
$ docker-compose up -d

# イメージの確認
$ docker image ls -a
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
mysql        latest    21978c3803ca   33 hours ago   544MB
node-node    latest    fd515a4c08ce   42 hours ago   1.05GB

# コンテナの確認
$ docker container ls -a
CONTAINER ID   IMAGE          COMMAND                  CREATED              STATUS              PORTS                               NAMES
64a2a554be28   mysql:latest   "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:3306->3306/tcp, 33060/tcp   mysql
db7d575fd078   node-node      "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:3000->3000/tcp              node

9. DBの確認

DBコンテナに入り、いろいろ中身を確認してみよう。

$ docker-compose exec db /bin/bash

# 認証プラグインの確認(先述のハッシュ方式の確認)
$ mysql -u root -p
> SELECT user, host, plugin FROM mysql.user;
+------------------+-----------+-----------------------+
| user             | host      | plugin                |
+------------------+-----------+-----------------------+
| root             | %         | mysql_native_password |
| ユーザー名         | %         | mysql_native_password |
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session    | localhost | caching_sha2_password |
| mysql.sys        | localhost | caching_sha2_password |
| root             | localhost | mysql_native_password |
+------------------+-----------+-----------------------+
6 rows in set (0.00 sec)


# タイムゾーンの確認
> show variables like '%time_zone%';
+------------------+------------+
| Variable_name    | Value      |
+------------------+------------+
| system_time_zone | UTC        |
| time_zone        | Asia/Tokyo |
+------------------+------------+
2 rows in set (0.02 sec)

# 文字コードの確認
> show variables like '%char%';
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | utf8mb4                        |
| character_set_connection | utf8mb4                        |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | utf8mb4                        |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8mb3                        |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.00 sec)

# テーブルとデータの初期化確認
> use DB名;
> show tables;
+-------------------+
| Tables_in_sandbox |
+-------------------+
| authors           |
| books             |
+-------------------+
2 rows in set (0.01 sec)
> SELECT b.id id, b.name name, a.name author FROM books b LEFT JOIN authors a ON b.author = a.id;
+----+-----------------------+---------------+
| id | name                  | name          |
+----+-----------------------+---------------+
|  1 | 吾輩は猫である        | 夏目 漱石     |
|  2 | 坊つちやん            | 夏目 漱石     |
|  3 | 三四郎                | 夏目 漱石     |
|  4 | 舞姫                  | 森 鷗外       |
|  5 | 雁                    | 森 鷗外       |
|  6 | 高瀬舟                | 森 鷗外       |
|  7 | にごりえ              | 樋口 一葉     |
|  8 | 十三夜                | 樋口 一葉     |
|  9 | たけくらべ            | 樋口 一葉     |
+----+-----------------------+---------------+
9 rows in set (0.00 sec)

よしよし。

10. デバッグサーバーの起動

Nodeコンテナでデバッグサーバーを起動する。

$ docker-compose exec node /bin/bash

# アプリケーションディレクトリへ移動
$ cd app-next

# デバッグサーバー起動
$ npm run dev

> app-next@0.1.0 dev
> next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry

event - compiled client and server successfully in 3s (170 modules)
wait  - compiling...
event - compiled successfully in 78 ms (137 modules)

11. 動作確認

http://localhost:3000をブラウザで動作確認。

ここまでは問題なさそう。

アプリケーション作成手順

1. パッケージのインストール

アプリケーションに必要なパッケージをインストールする。Dockerfileでグローバルにインストールしたくないけど、docker-compse.ymlに初回のみ実行されるコマンドとして登録するのも面倒なので手動でやるか。

MySQLのクライアントと環境変数ファイル読み込みパッケージを導入する。

$ docker-compose exec node /bin/bash

# アプリケーションディレクトリへ移動
$ cd app-next

# MySQLクライアントパッケージ
$ npm install promise-mysql

# 環境変数ファイル読み込みパッケージ
$ npm install dotenv

2. APIを作成

app-next/app/api/books/route.tsを作成し、DBから取得した値を返してみる。(今回はTypeScriptを使用)

import { NextResponse } from 'next/server';
import * as mysql from 'promise-mysql';

// 環境変数
require('dotenv').config({path: '../.env'});

// 書籍一覧取得API
export async function GET() {
  const connection = await mysql.createConnection({
    host: 'db',
    port: 3306,
    database: process.env.MYSQL_DATABASE,
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD
  });

  const sql = 'SELECT b.id id, b.name name, a.name author FROM books b LEFT JOIN authors a ON b.author = a.id';
  const result = await connection.query(sql);
  connection.end();

  return NextResponse.json(result);
}

3. APIの動作確認

http://localhost:3000/api/booksで作成したAPIを呼び出してみる。

[
  {
    "id":1,
    "name":"吾輩は猫である",
    "author":"夏目 漱石"
  },
  {
    "id":2,
    "name":"坊つちやん",
    "author":"夏目 漱石"
  },
  ...
}

いい感じ。

4. コンポーネントの作成

app-next/app/pages/books/page.tsxを作成し、APIから取得した値を当ててみる。(今回はTypeScriptを使用)

// 型宣言
type Book = {
  id: number,
  name: String,
  author: String
}
 
 // コンポーネント
 export default async function Books() {
  // データ取得
  const response = await fetch('http://localhost:3000/api/books');
  const books: Book[] = await response.json();
  // 描画
  return (
    <>
      <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
        rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
        crossOrigin="anonymous">
      </link>
      <table className="table">
        <thead>
          <tr>
            <th>ID</th>
            <th>書名</th>
            <th>著者</th>
          </tr>
        </thead>
        <tbody>
          {books.map(book => (
            <tr key={book.id}>
              <th>{book.id}</th>
              <td>{book.name}</td>
              <td>{book.author}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

テーブルの見映えを良くするためにBootstrapをあててみた。

5. 動作確認

http://localhost:3000/booksをブラウザで動作確認。

おしまい。

この記事が気に入ったらサポートをしてみませんか?