Docker Compose(複数のコンテナをまとめて起動・管理するツール)でAPIとPostgresを立ち上げたとき、docker compose psでは両方ともUpなのに、APIログには「localhost:5432に接続できない」と表示される状況に遭遇することがあります。
この記事は、フロントエンドからバックエンドまで含めたローカル開発環境をDocker Composeで組んでいるエンジニアに向けた内容です。コンテナ間通信でつまずいた経験がある方の確認作業の参考になれば幸いです。
何が起きているか
症状はいつも似ています。docker-compose.ymlにportsは書いてある。コンテナのステータスも正常。それでもアプリケーション側は接続エラーを吐き続けます。
これはPostgresが落ちているわけでも、ポート番号を書き間違えたわけでもありません。接続先のホスト名としてlocalhostを指定していることが原因です。
影響範囲は意外と広く、API単体のバックエンド構成だけでなく、フロントエンドのビルドツール(Vite、Next.jsなど)が開発中にバックエンドAPIへプロキシする設定でも同じ罠にはまります。Docker Compose初心者がまず最初にぶつかる典型的な壁といえます。
なぜ起きるのか
原因を分解すると、Dockerのネットワークモデルに対する誤解に行き着きます。
コンテナごとに独立したネットワーク名前空間がある
localhost(ループバックアドレス、127.0.0.1を指す特別なホスト名)は「自分自身」を意味します。ただしこの「自分自身」は、ホストマシンではなくコンテナ自身を指します。
つまりAPIコンテナの中でlocalhost:5432に接続しようとすると、APIコンテナ自身のポート5432を探しにいきます。Postgresは別のコンテナで動いているため、そこには何もありません。ホストマシンでlocalhostと書く感覚をそのままコンテナ内に持ち込むと破綻します。
Composeが自動生成するネットワークとサービス名解決
docker compose upを実行すると、Composeはプロジェクト用のネットワークを自動作成します。ディレクトリ名がmyappなら、ネットワーク名はおおよそmyapp_defaultになります。
docker-compose.ymlにnetworks:を一行も書いていなくても、Composeが裏で作業しています。このネットワークに参加しているサービス同士は、コンテナのIPアドレスではなくサービス名で互いを見つけられます。
たとえばAPIからPostgresへ接続する場合、接続文字列のホスト部分はdb(docker-compose.ymlでPostgresサービスに付けた名前)になります。
services:
api:
build: ./api
environment:
DATABASE_URL: postgresql://myapp:secret@db:5432/myapp
db:
image: postgres:16-alpineコンテナのIPアドレスを直接書く方法もありますが、これは避けるべきです。コンテナが再作成されるたびにIPアドレスは変わる可能性があるためです。サービス名はDNS解決の仕組みでコンテナ内部から名前解決されるため、再起動後も変わりません。
portsマッピングの役割を誤解している
もう一つのつまずきポイントは、ports: \"3000:3000\"のようなポートマッピングの役割です。これはホストマシンからコンテナへアクセスするための設定であり、コンテナ同士の通信には関係ありません。
コンテナ同士はCompose内部のネットワークで直接通信できるため、portsを書かなくても、サービス名とコンテナ内部のポート番号(db:5432など)で到達できます。portsを書き忘れたせいで接続できないと勘違いし、無関係な設定をいじってしまうケースも見られます。
自分のプロジェクトが該当するか確認する方法
実際に自分の環境で該当するかどうかは、次の手順で確認できます。
# 1. どのネットワークが作られているか確認する
docker network ls
# 2. docker-compose.yml内の接続文字列・環境変数を確認する
grep -i \"localhost\" docker-compose.yml
# 3. コンテナ内部から実際に名前解決できるか確認する
docker compose exec api sh -c \"getent hosts db || ping -c 1 db\"grepで環境変数やDATABASE_URLの中にlocalhostや127.0.0.1が残っていれば、それがコンテナ間通信を阻害している可能性が高いです。ホストマシンのブラウザからアクセスする設定(公開ポート経由)と、コンテナ同士の内部通信設定を混同していないか、該当箇所を見比べてください。
対策の手順
対策はシンプルです。接続先をコンテナ間通信用とホスト公開用で明確に書き分けます。
- アプリケーション同士の接続文字列・環境変数では、
localhostの代わりにCompose上のサービス名を使う(例:db、redis、api) - ブラウザやローカルのcurlからアクセスする場合のみ
localhost:公開ポート番号を使う portsはホスト公開が必要なサービスにだけ付ける(DBをホストから直接覗きたい場合を除き、必須ではない)docker-compose.ymlを変更したらdocker compose down && docker compose up -dで再作成し、古いネットワーク設定を引きずらないようにする- 複数のCompose構成を同時に動かす場合は、プロジェクト名が衝突していないか
docker compose lsで確認する
接続先をホスト名別に整理すると次のようになります。
| 接続元 | 接続先 | 正しいホスト名 |
|---|---|---|
| ブラウザ | 公開されたAPI | localhost:公開ポート |
| APIコンテナ | Postgresコンテナ | db(サービス名) |
| APIコンテナ | Redisコンテナ | redis(サービス名) |
| 任意のコンテナ | 別のコンテナ | 相手のサービス名 |
localhostは「そのコンテナ自身」を指すため、コンテナ間通信には必ずサービス名を使います。フロントエンド開発でViteやwebpack-dev-serverのプロキシ設定をDocker Compose環境に持ち込む場合も同じ考え方です。プロキシ先のターゲットURLにホストマシンのlocalhostをそのまま書くと、コンテナ化した途端に同じエラーが再発します。開発サーバーをコンテナで動かすなら、プロキシターゲットもサービス名ベースに書き換える必要があります。
まとめ
Docker Composeのネットワーキングでつまずく原因は、だいたい次の3点に集約されます。
- コンテナ内の
localhostはコンテナ自身を指し、ホストマシンや他のコンテナを指さない - Composeは自動でプロジェクト用ネットワークを作り、サービス名で名前解決できる仕組みを提供している
portsマッピングはホストからのアクセス用であり、コンテナ間通信には不要
まずdocker network lsとgrep -i localhost docker-compose.ymlを実行して、自分の構成に古い設定が残っていないか確かめてみてください。該当箇所が見つかったら、サービス名への置き換えと再起動で解消できるはずです。