Compose で複数サービスをつなぐ — Docker Tips
Compose はサービスを複数並べるための道具です。web(nginx)と api(小さな Python HTTP)の 2 つだけで、名前解決と depends_on の限界、既定ネットワークを確かめます。DB やクラウドは使いません。
参考: Services · Networking in Compose · Startup order · Networks
目次
- 作る構成の概要
- compose.yaml を用意する
- 起動してポートを確認する
- サービス名で名前解決する
- depends_on が保証すること・しないこと
- service_healthy でもう一歩確実にする
- 既定のネットワーク(bridge)を覗く
- 片付け
- よくある失敗
作る構成の概要
| サービス | イメージ | 役割 |
|---|---|---|
web |
nginx:alpine |
ホストにポートを公開する「入口」役 |
api |
python:3.12-alpine |
python -m http.server で待ち受ける簡易サーバー |
web から api に対して、コンテナの IP アドレスを知らなくても サービス名 api で直接アクセスできる ことを確認するのが目的です。
flowchart LR Host["ホスト<br/>localhost:8080"] --> Web["web (nginx)"] Web -- "http://api:8000" --> Api["api (python http.server)"] subgraph net["Compose の既定ネットワーク(bridge)"] Web Api endcompose.yaml を用意する
任意の作業フォルダに compose.yaml を作成します(docker-compose.yml でも動作は同じですが、Compose Specification に合わせて compose.yaml を使います。version: キーは Compose Specification では不要かつ obsolete のため書きません)。
services: api: image: python:3.12-alpine working_dir: /tmp command: ["sh", "-c", "sleep 8 && python -m http.server 8000"] web: image: nginx:alpine depends_on: - api ports: - "8080:80"| 設定 | 意味 |
|---|---|
api.command |
起動直後に わざと 8 秒待ってから HTTP サーバーを立てる(後述の検証用) |
web.depends_on: [api] |
api を先に起動してから web を起動する |
web.ports |
ホストの 8080 を web(nginx)の 80 に公開 |
api にはポート公開を設定していません。これは コンテナ間だけで通信できれば十分 で、ホストから直接 api を叩く必要がないケースを想定しているためです。ホストの外に出ないサービス(内部 API・ワーカーなど)はこのように ports を省略できます。
起動してポートを確認する
docker compose up -ddocker compose ps| 列 | 見るポイント |
|---|---|
NAME |
<フォルダ名>-web-1 / <フォルダ名>-api-1 のような名前になる |
STATUS |
両方が Up になっているか |
PORTS |
web に 0.0.0.0:8080->80/tcp が付いているか |
ブラウザで http://localhost:8080 を開けば nginx の Welcome ページが表示されます。単体の docker run -p で nginx を動かしたときと同じ確認です。
サービス名で名前解決する
Compose は起動時に プロジェクト専用のネットワーク を自動で作り、各サービスをそのネットワークに参加させます。参加したコンテナは、IP アドレスを調べなくても サービス名がそのままホスト名になる ため、名前で直接通信できます。
api の 8 秒スリープが終わったあとに、web コンテナの中から api へアクセスしてみます。
docker compose exec web wget -qO- http://api:8000nginx:alpine は Alpine ベースで、busybox 由来の wget が同梱されているため追加インストールなしで確認できます。しばらくすると次のような一覧がテキストで返ってきます(python -m http.server がカレントディレクトリの一覧を返すため)。
<!DOCTYPE html><html><head><title>Directory listing for /</title></head>...このとき web は api の IP アドレスを一切知らずに、サービス名 api だけで到達 しています。これが Compose の既定ネットワークが提供する DNS 解決です。
depends_on が保証すること・しないこと
depends_on は便利ですが、保証する範囲は狭いです。
| 保証する | 保証しない |
|---|---|
| 依存先コンテナを 先に起動開始する | 依存先の中の アプリが接続を受け付けられる状態 になっていること |
docker compose down 時に 依存元を先に止める(逆順で停止) |
ポートが実際に listen しているか |
先ほどの compose.yaml では api が sleep 8 してから HTTP サーバーを立てています。もし docker compose up -d の 直後(8 秒以内)に web から api へアクセスすると、次のように失敗します。
docker compose up -ddocker compose exec web wget -qO- http://api:8000wget: can't connect to remote host (172.x.x.x:8000): Connection refuseddepends_on: [api] によって api コンテナ自体は先に 起動開始 していますが、その中の python -m http.server はまだ sleep 8 の最中で listen していません。「コンテナが起動した」と「アプリが準備完了した」は別物 であることを、この短い例で確認できます。
service_healthy でもう一歩確実にする
「準備完了を待ちたい」場合は、healthcheck と depends_on の condition: service_healthy を組み合わせます。
services: api: image: python:3.12-alpine working_dir: /tmp command: ["sh", "-c", "sleep 8 && python -m http.server 8000"] healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8000"] interval: 2s timeout: 2s retries: 10 start_period: 15s web: image: nginx:alpine depends_on: api: condition: service_healthy ports: - "8080:80"| 設定 | 意味 |
|---|---|
healthcheck.test |
コンテナ内で定期的に実行し、成功すれば healthy とみなすコマンド |
depends_on.api.condition: service_healthy |
api が healthy になるまで web の起動を待つ |
これで docker compose up -d は api が healthy になるまで web を作成しません。condition: service_healthy は対象サービスの healthcheck 結果に依存するため、healthcheck を定義していないサービスに付けると、多くの場合 docker compose up 時点でエラーになり起動が止まります。healthy 待ちを使うなら、先に対象サービスへ healthcheck を書いてください。
既定のネットワーク(bridge)を覗く
明示的に networks: を書かなくても、Compose は <プロジェクト名>_default という名前のブリッジネットワークを自動生成し、全サービスをそこに参加させます。
docker network lsdocker network inspect <フォルダ名>_defaultinspect の出力の Containers セクションに web と api の両方が載っていれば、同じネットワークに参加していることが確認できます。サービスを増やしたいときは compose.yaml に services: 配下へ新しいキーを追記するだけで、そのサービスも自動的に同じ既定ネットワークへ参加します。
片付け
docker compose downイメージまで削除したい場合は次を使います。この例では image: を明示しているため、--rmi local ではなく --rmi all を使います。
docker compose down --rmi all -v-v は名前付きボリュームも削除します(この例ではボリュームを使っていないため付けても付けなくても差はありません)。--rmi local は image: 未指定で自動生成されたイメージ向けです。
よくある失敗
| 症状 | 原因 | 対処 |
|---|---|---|
wget: can't connect ... Connection refused |
api がまだ listen していない(depends_on は起動順のみ保証) |
数秒待つ、または healthcheck + condition: service_healthy を使う |
wget: bad address 'api' |
別ネットワークに乗っていて名前解決できない、または typo | サービス名の綴りを確認、docker network inspect で参加コンテナを確認 |
docker compose exec web wget: not found |
使っているベースイメージに wget が入っていない |
nginx:alpine を使う、または curl が入ったイメージに変える |
depends_on を書いたのに接続エラーが出る |
depends_on は起動順序のみで、readiness は保証しない |
service_healthy 条件か、アプリ側にリトライ処理を入れる |
ポート 8080 が使われている |
別コンテナや別アプリが使用中 | docker ps --filter "publish=8080" で確認し停止、または別ポートに変更 |
docker compose down 後も <フォルダ名>_default ネットワークが残る |
他のプロジェクトや停止していないコンテナが参加している | docker network ls で確認し、不要なら docker network rm |
depends_on は「順番」の制御であり、「準備完了」の制御ではない——この一点を理解しておくと、複数サービスを組んだときの接続エラーの大半に見当がつくようになります。