Docker Compose: データベースマイグレーションとスキーマ更新を自動化するテクニック

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

問題:アプリがデータベースより先に「走り出してしまう」とき

初めてDocker Composeを使って実際のプロジェクトをデプロイした際、私は非常に初歩的なミスで苦い経験をしました。docker-compose up -d を実行し、Dockerがすべてのコンテナを「Started」と緑色で表示したのを見て、意気揚々とウェブサイトにアクセスしました。しかし、目の前に現れたのは **Internal Server Error (500)** という現実でした。

ログを確認すると、見慣れたメッセージが表示されていました:Relation "users" does not exist。データベースのコンテナは動いていても、テーブル構造(スキーマ)が空のままだったり、更新が間に合っていなかったりしたのです。その時は、手動でコンテナに docker exec してマイグレーションを実行するしかありませんでした。一度ならまだしも、本番環境やCI/CDシステムでこれを10回も繰り返すのは、まさに惨劇です。

なぜ depends_on は「口約束」に過ぎないのか?

初心者(そして昔の私)がよく犯す間違いは、depends_on: - db を盲信してしまうことです。実際、Docker Composeが保証するのは、DBコンテナがアプリコンテナより **先に起動を開始する** ことだけです。

しかし、コンテナが「実行中」であることは、データベースエンジンが接続を受け入れる準備ができていることを意味しません。PostgreSQLのインスタンスは、メモリの初期化やシステムファイルのチェックに通常5〜10秒かかります。一方で、Node.jsやGoのアプリは1秒足らずで起動します。その結果、アプリは「起動中」で忙しいDBに接続を試み、即座にクラッシュしてしまいます。

マイグレーションを自動化するための3つの実践的な解決策

この問題を解決するには、メインのアプリケーションを動作させる前に、DBの本当の状態を確認する仕組みが必要です。

1. Entrypointスクリプトを使用する(軽量かつ一般的)

npm start で直接起動する代わりに、シェルスクリプトでラップします。このスクリプトは門番のような役割を果たします:DBのポートが開くのを待つ -> マイグレーションを実行する -> アプリを起動する。

これは私がNode.jsプロジェクトでよく使用する entrypoint.sh ファイルです:

#!/bin/sh

# データベースの5432ポートが開くのを待機
echo "Waiting for postgres..."
while ! nc -z db 5432; do
  sleep 0.1
done

echo "PostgreSQL is up - executing migrations"

# スキーマの更新を実行
npm run db:migrate

# メインアプリケーションに制御を渡す
exec "$@"

Dockerfile で実行権限を与えるのを忘れないでください:

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["npm", "start"]

2. wait-for-it ツールの活用

手動でスクリプトを書くのが面倒な場合は、wait-for-it.sh が最適な選択肢です。このツールは非常に安定しており、タイムアウト機能もサポートしているため、DBに問題が発生した際にコンテナが無限に待機し続けるのを防げます。

docker-compose.yml での設定は以下のようになります:

services:
  app:
    build: .
    command: ["./wait-for-it.sh", "db:5432", "--timeout=30", "--", "npm", "run", "migrate-and-start"]
    depends_on:
      - db

3. マイグレーションを別サービスに分離する(プロフェッショナルな手法)

水平スケーリング(複数のアプリコンテナを同時に実行)が必要な大規模システムでは、起動プロセスにマイグレーションを詰め込むとリソースの無駄になります。最善の方法は、マイグレーションを一度だけ実行される「One-offコンテナ」として分離することです。

services:
  db:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s

  migration:
    build: .
    command: npm run db:migrate
    depends_on:
      db:
        condition: service_healthy

  app:
    build: .
    command: npm start
    depends_on:
      migration:
        condition: service_completed_successfully

動作フローは非常に厳密です:Dockerは db がヘルスチェックをパスするのを待ち、その後 migration コンテナを起動します。マイグレーションが Exit Code 0(成功)で終了したときのみ、**app** がユーザーへのサービス提供を開始します。

データの消失を防ぐための実践的な経験則

何度もトラブル対応を経験した結果、覚えておくべき3つの黄金律を導き出しました:

  • 冪等性(Idempotent): マイグレーションスクリプトは、何度実行してもエラーにならないようにする必要があります。常に CREATE TABLE IF NOT EXISTS を使用するか、Flyway、Alembic、Sequelizeなどのライブラリを活用しましょう。
  • 常にバックアッププランを: 本番環境でマイグレーションを実行する前に、システムが自動的にDBのスナップショットを取得していることを確認してください。スクリプトが誤って DROP COLUMN を実行してしまった場合、Dockerはあなたを救えません。
  • ログは救世主: マイグレーションコンテナが明確なログを出力するようにしてください。docker-compose up が停止したとき、それがDBの準備不足によるものか、SQLの構文エラーによるものかを即座に判断する必要があります。

結びに:プロジェクトにどの方法を選ぶべきか?

個人プロジェクトや開発段階であれば、**方法1(Entrypoint)** が最も手軽な選択肢です。composeファイルをシンプルで分かりやすい状態に保てます。

一方、本番環境やKubernetesへのデプロイを目指すなら、**方法3(サービスの分離)** が必須の道です。責任を分離することで、システムの安定性が向上し、管理が格段に容易になります。スムーズなデプロイができることを願っています!

Share: