開発環境では問題なく動いていたのに、本番にデプロイしたら謎のエラーが出る。フルスタックをやっていると、この経験を一度はするはずだ。自分も何度かハマったので、Docker Compose まわりで実際に遭遇した「本番との差異」とその解決策をまとめておく。
環境変数の扱いでハマった
最初にやらかしたのが環境変数まわり。docker-compose.yml に env_file: .env と書いていたのだが、本番の ECS タスク定義では Secrets Manager や SSM Parameter Store から値を差し込んでいた。
問題は、ローカルの .env に書いた DATABASE_URL と本番の値でフォーマットが微妙に違ったこと。ローカルは localhost:5432、本番は Aurora のエンドポイントを指していて、SSL オプションの有無が異なっていた。アプリ側でコネクションエラーが出るまで気づかなかった。
# docker-compose.yml(開発用)
services:
app:
env_file:
- .env.local
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://user:pass@db:5432/mydb
解決策としては、環境変数のスキーマを zod や envalid で検証するようにした。起動時にバリデーションが走るので、フォーマットの違いが即座に検出できる。.env.example に本番と同じキー名・フォーマットのコメントを必ず書くようにもした。
ボリュームマウントが引き起こす「ファイル存在しない」問題
ソースコードをホストからマウントしているとき、node_modules もホスト側のものがそのまま使われる。コンテナ内のアーキテクチャ(linux/amd64)と Mac のホスト(arm64)でバイナリが異なるパッケージが混在したことがあった。
volumes:
- .:/app
- /app/node_modules # anonymous volume で上書きを防ぐ
/app/node_modules を anonymous volume にしてホスト側の干渉を防ぐのが定番の解決策だ。ただ package.json を更新したあとに docker-compose up だけやると古い node_modules が残ったまま起動してしまう。依存関係を変えたら必ず --build フラグをつける運用に変えた。
ネットワーク設定のデフォルトが思ったより違う
本番環境(ECS + ALB)では、コンテナ間通信はプライベート IP や内部 DNS で解決していた。一方、Docker Compose はデフォルトで同一ネットワーク内のサービス名を DNS として使えるので、http://api:3000 のような書き方がそのまま動く。
本番移行後にフロントエンドからバックエンドへの通信がタイムアウトした原因がこれだった。ECS の場合はサービスディスカバリの設定が別途必要で、Compose と同じ感覚でサービス名を書いても解決されない。接続先のホスト名を環境変数に外出しして、環境ごとに切り替えられるようにするのが一番シンプルな対策だ。
まとめ
- 環境変数はスキーマ検証を入れ、フォーマットを統一する
node_modulesのボリューム管理は意図的に設計する- ネットワーク解決の仕組みはローカルと本番で別物と思っておく
「ローカルで動いた」を信頼しすぎず、本番に近い設定でテストできる CI 環境を整備するのが長期的には一番コストが低い。