開発環境のMySQL Dockerize化:Init Scriptsでスキーマとサンプルデータを自動初期化する

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

5分でセットアップ完了 — さっそく始めよう

チームに新しいメンバーが入るたびに、同じ光景が繰り返される。新しい開発者がMySQLのインストール、データベースの作成、スキーマのインポート、シードデータの実行に午前中を丸々費やして、まだ一行もコードを書いていない。あるいはもっと悪いことに、各自の開発環境がバラバラで、このマシンでは再現できるのにあのマシンでは再現できないバグが発生する。

Docker + Init Scriptsがその問題をすっきり解決してくれる。MySQLコンテナが初回起動時にスキーマとシードデータを自動生成する。docker compose upだけで完了 — それ以上の操作は不要。チームでこの方法を採用してから、新しい開発者の環境セットアップ時間が2〜3時間から約10分(イメージのpull待ち)に短縮された。

このディレクトリ構造を作成する:

project/
├── docker-compose.yml
└── mysql/
    ├── init/
    │   ├── 01_schema.sql
    │   └── 02_seed_data.sql
    └── conf/
        └── my.cnf

ファイル docker-compose.yml

services:
  db:
    image: mysql:8.0
    container_name: myapp_db
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: myapp
      MYSQL_USER: devuser
      MYSQL_PASSWORD: devpass
    ports:
      - "3306:3306"
    volumes:
      - ./mysql/init:/docker-entrypoint-initdb.d
      - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

ファイル mysql/init/01_schema.sql

CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

ファイル mysql/init/02_seed_data.sql

INSERT INTO users (username, email) VALUES
    ('alice', '[email protected]'),
    ('bob', '[email protected]');

INSERT INTO posts (user_id, title, body) VALUES
    (1, 'Hello World', 'Aliceの最初の投稿'),
    (2, 'Docker is great', 'BobによるDockerの紹介');

実行してみよう:

docker compose up -d
# 約10秒待ってから確認
docker exec -it myapp_db mysql -u devuser -pdevpass myapp -e "SELECT * FROM users;"

スキーマとデータがすでに用意されている。リポジトリをcloneしてコマンド一つ実行するだけ — Windows、Mac、Linuxどのマシンでも同じように動く。

動作の仕組み — 想定外を防ぐために理解しておこう

/docker-entrypoint-initdb.dディレクトリは、MySQLの公式イメージが初期化スクリプトを受け入れる場所だ。コンテナが初めて起動してボリュームにデータがない場合、エントリポイントスクリプトがそのディレクトリ内のすべてのファイルをアルファベット順に自動実行する。それだけ — 何も魔法はない。bashのfor f in /docker-entrypoint-initdb.d/*ループにすぎない。

使う前に押さえておくべき4つのポイント:

  • 実行順序:アルファベット順 — だから01_schema.sql02_seed_data.sqlという命名にしている。スキーマはシードデータより先に実行しなければ、外部キーの設定が失敗する。
  • 一度だけ実行:ボリュームに前回のデータがある場合、Init Scriptsは完全にスキップされる。この動作は正しい — コンテナ再起動時に実データを上書きしないよう保護している。
  • .sqlと.shに対応:同じディレクトリにSQLファイルとシェルスクリプトを混在させることができ、ファイル名順に交互に実行される。
  • デフォルトのデータベースが選択済みMYSQL_DATABASE変数がスクリプト実行時のデータベースコンテキストを決定する — 各SQLファイルの先頭にUSE myapp;を追加する必要はない。

Init Scriptsを最初からリセットして再実行したい場合は、ボリュームごと削除する:

docker compose down -v   # コンテナとボリュームを削除
docker compose up -d     # 最初から再作成、Init Scriptsが再実行される

応用編 — より実践的なシナリオ

純粋なSQLの代わりにシェルスクリプトを使う

複数のデータベースを作成する、大きなダンプファイル(数百MB)をインポートする、または条件付きロジックを実行する必要がある場合、純粋なSQLでは不十分だ。シェルスクリプトならすべて対応できる:

#!/bin/bash
# mysql/init/03_extra_setup.sh

set -e

mysql -u root -p"$MYSQL_ROOT_PASSWORD" <<-EOSQL
    CREATE DATABASE IF NOT EXISTS myapp_test;
    GRANT ALL PRIVILEGES ON myapp_test.* TO 'devuser'@'%';
EOSQL

echo "追加セットアップ完了"

.shファイルには実行権限が必要 — この手順はよく忘れられる:

chmod +x mysql/init/03_extra_setup.sh

開発環境向けのMySQL設定のカスタマイズ

ファイル mysql/conf/my.cnf でMySQLの厳格な制約を緩和できる。本番環境ではstrictが必要でも、開発環境では必ずしもそうではない:

[mysqld]
# strict modeを緩和して開発しやすくする
sql_mode = ONLY_FULL_GROUP_BY,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO

# バイナリログを無効化 — 開発環境ではレプリケーション不要、ディスク節約
skip-log-bin

# 複数のサービスが接続する場合に備えてmax_connectionsを増加
max_connections = 200

# タイムゾーン
default-time-zone = '+07:00'

起動状態のヘルスチェック

バックエンドがMySQLより先に起動するのはよくあるミス — DBに接続できずアプリがすぐにクラッシュする。ヘルスチェックで簡単に解決できる:

services:
  db:
    image: mysql:8.0
    # ... その他の設定 ...
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

  backend:
    image: myapp_backend
    depends_on:
      db:
        condition: service_healthy

condition: service_healthyにより、MySQLが実際に接続を受け付けるまでバックエンドが起動しないことが保証される。エントリポイントでsleep 30ハックを使う必要はもうない。

日々の使用経験から得た実践的なTips

スキーマとシードデータを分離する

スキーマ(DDL)とシードデータ(DML)は別々のファイルに置く — シンプルに聞こえるが、思っているより重要な原則だ。スキーマが変わった時01_schema.sqlだけ修正し、データには触れない。テストケースを追加する時は02_seed_data.sqlだけ修正する。git diffがすっきりし、コードレビューが楽になり、バグが発生した時も問題の切り分けが容易になる。

docker-compose.ymlにパスワードをハードコードしない

.envファイルを使う:

# .env (.gitignoreに追加)
MYSQL_ROOT_PASSWORD=your_root_password
MYSQL_PASSWORD=your_dev_password
# docker-compose.yml
environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  MYSQL_PASSWORD: ${MYSQL_PASSWORD}

シンプルなルール:プレースホルダーの値を持つ.env.exampleはgitにcommitする。実際の.envはgitignoreに追加する。例外なし — プライベートリポジトリでも同様。

シードデータは冪等性を持たせる

純粋なINSERTの代わりにINSERT IGNOREまたはINSERT ... ON DUPLICATE KEY UPDATEを使う。何度実行してもエラーにならない:

INSERT IGNORE INTO users (username, email) VALUES
    ('alice', '[email protected]'),
    ('bob', '[email protected]');

これは部分的なバックアップから復元する場合に特に重要だ — Init Scripts全体を再実行する代わりに、シードデータだけを再実行してもduplicate keyエラーを心配する必要がない。

痛い経験から学んだバックアップの重要性

午前3時。突然ディスクがフルになった。MySQLが書き込み途中でファイルが破損し、データベースが壊れた。バックアップから復元しながら手が震えていた — その時初めて、なぜバックアップがそれほど重要なのかを本当に理解した。それ以来、docker-composeにmysqldumpを毎日cronで実行するサービスを追加している:

  db_backup:
    image: mysql:8.0
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./backups:/backups
    entrypoint: |
      sh -c 'while true; do
        mysqldump -h db -u root -p$$MYSQL_ROOT_PASSWORD myapp > /backups/myapp_$$(date +%Y%m%d_%H%M).sql;
        find /backups -name "*.sql" -mtime +3 -delete;
        sleep 86400;
      done'

5分のセットアップで直近3日分のバックアップを保持し、古いものを自動削除。小さな仕組みだが、何度も助けられてきた。

デバッグ時のInit Scriptのログ確認

Init Scriptがエラーで終了するが原因が不明な場合は、コンテナのログを確認する:

# コンテナ起動時のログをすべて表示
docker compose logs db

# リアルタイムで追跡し、重要な行をフィルタリング
docker compose logs -f db 2>&1 | grep -E "(ERROR|init|schema)"

MySQLは実行中の各ファイルと具体的なエラー行を明確にログに記録する — たいてい数分でデバッグが終わる。

このセットアップはMySQLを使うすべてのプロジェクトに適用している。個人のサイドプロジェクトから10人チームまで。新しい環境を作るたびにかかる時間はイメージをpullする時間だけ — 手動でテーブルを一つひとつ作ったり「スキーマはもうimportした?」と互いに確認し合う光景はもうなくなった。

Share: