
[docker-compose.ymlにappとdbの二つのサービスを書いて、docker compose](/articles/dockerdao-runofen-qi-dian-ge/) upを実行した瞬間——appコンテナからdbコンテナへ、dbという文字列だけで接続できてしまう。この「なぜか動く」体験に、ふと不思議さを感じたことはありませんか。ローカル開発ではもう当たり前になっている挙動ですが、裏側ではDockerがいくつものプロセスを順序立てて動かし、コンテナ同士の名前解決を整えています。本日はそのプロセスと仕組みを、順を追って丁寧におさえていきましょう。
プロジェクトごとに作られる専用ネットワークの裏側
docker compose upを最初に実行したタイミングで、Docker Composeはプロジェクト専用のユーザー定義ブリッジネットワークを自動で一つだけ作成します。ネットワーク名は<プロジェクト名>_defaultというルールで生成されるため、たとえばディレクトリ名(プロジェクト名)がmyappであればmyapp_defaultという名前になります。docker network lsを実行すると、このネットワークが他のデフォルトネットワークとは別に並んでいるのが確認できるはずです。
ここで注意していただきたいのは、このネットワークが「ユーザー定義」であるという点です。Dockerをインストールした直後、docker network lsにはbridgeという名前のネットワークが表示されますが、こちらはDockerデーモンが最初から用意しているデフォルトブリッジです。Composeが作成するネットワークとは、見た目はどちらもブリッジで同じでも、通信と名前解決のルールが大きく異なります。
具体的に申し上げると、デフォルトブリッジに接続されたコンテナは、IPアドレスでしか他のコンテナにアクセスできません。コンテナ名やサービス名を指定しても、DockerはそれをIPに変換してくれないのです。一方、Composeが作るユーザー定義ネットワークに接続されたコンテナは、サービス名やコンテナ名でお互いを指定でき、しかも名前解決が自動で行われます。
「なるほど、だからdocker runで立ち上げた単一のコンテナ同士はIPでしか繋がらないのか」——そう思っていただけると、当たり前だと思っていたComposeの挙動が少し違って見えてくるのではないでしょうか。下の表に、両者の違いを整理しておきます。
| 観点 | デフォルトブリッジ(bridge) | Composeが作るユーザー定義ネットワーク |
|---|---|---|
| 自動作成のタイミング | Dockerインストール直後 | docker compose up実行時 |
| ネットワーク名の例 | bridge | <プロジェクト名>_default |
| コンテナ間の指定方法 | IPアドレスのみ | サービス名・コンテナ名で指定可能 |
| 内蔵DNSサーバー(127.0.0.11)による名前解決 | サポートされない | サポートされる |
| 主な用途 | レガシーな単一コンテナ実行 | Composeによる複数サービス構成 |
この表を見ると、Composeがネットワーク分離の観点で最初から設計されていることが伝わります。デフォルトブリッジはあくまで後方互換のための仕組みであり、現代的なコンテナ運用ではComposeのネットワークを使うのが基本線だと考えておくと、判断がぶれにくくなります。
内蔵DNSサーバー127.0.0.11がコンテナ内で動く仕組み
ユーザー定義ネットワークに接続されたコンテナの内側を覗いてみると、/etc/resolv.confというファイルが自動的に書き換えられています。通常のLinuxコンテナであれば、このファイルにはホストOSのDNSサーバーアドレスが記載されているはずです。しかしComposeで立ち上げたコンテナでは、nameserver 127.0.0.11という一文が自動で追加されています。
127.0.0.11はループバックアドレスと呼ばれる特別なIPで、コンテナ自身の中を指しています。よくある誤解として「コンテナ内にDNSサーバーが動いている」と思い浮かべる方もいらっしゃるかもしれませんが、実際にはそのサーバープロセスはコンテナの中にはいません。127.0.0.11宛のパケットは透過的にDockerデーモン側へ転送され、デーモン上で動作する内蔵DNSサーバーが応答を返します。これはDocker 1.10(2016年リリース)以降の標準的なプロセスです。
127.0.0.11はコンテナ自身を指すアドレスですが、実際に応答するDNSサーバーはコンテナの外、Dockerデーモンのプロセス空間で動いています。
このあたりの仕様は、後から設定を見直したときに「あれ、コンテナ内にnamedが動いていないのに、なぜdbという名前が引けるんだろう」と疑問を抱きやすいポイントです。DNSサーバーの所在を頭の片隅に置いておくと、トラブルシューティングの道筋を立てるときに迷子になりにくくなります。
もう一点、ComposeとDNSの組み合わせで押さえておきたいのは、ポート53番がそのまま利用されるという点です。コンテナ内からdbへの名前解決が行われるとき、appコンテナのDNSリゾルバは127.0.0.11の53番ポートに問い合わせを送り、Dockerデーモンの内蔵DNSサーバーが応答するという流れが成立しています。普段は意識しないDNSの標準ポートが、Composeのプロセスの中でちゃんと活きている、というわけです。
Dockerデーモンが行う動的な名前解決のプロセス
それでは、内蔵DNSサーバーは具体的にどのようにコンテナ名とIPアドレスを対応付けているのでしょうか。プロセスはシンプルで、デーモンはコンテナの起動と停止のタイミングに合わせて、メモリ上の名前解決テーブルを更新しています。
具体的には、コンテナが起動すると、そのコンテナ名(あるいはComposeで定義したサービス名)とIPアドレスの組み合わせが、内蔵DNSサーバーの管理テーブルにAレコードとして登録されます。コンテナが停止したときには、該当するレコードが即座に削除されます。つまり、「コンテナが立ち上がったら疎通できる、止めたら疎通できなくなる」という直感的な挙動は、この動的なレコード更新プロセスによって支えられているのです。
docker-compose.ymlに書いたappやdbといったサービス名は、内部的にはこのAレコードのキーとして使われています。docker compose psで確認できるCONTAINER IDとNAMEの組み合わせが、そのままDNSのレコードに対応していると考えると、挙動の理解が一段深まります。
ここで一点、先回りして補足しておきたい再現性の話をします。Composeが動的にネットワークを作る以上、コンテナ名やIPアドレスは起動のたびに変わる可能性があります。docker-compose.ymlに書いたサービス名は安定して使えますが、コンテナ個別の名前(docker compose run --nameで指定するもの)に依存したスクリプトを書いてしまうと、再現性が崩れてしまうことがあります。原則として、コンテナ間通信にはサービス名を使うこと。これが現場でよく言われる鉄則であり、ネットワークの仕組みを理解したうえでのおすすめポイントです。
スケールアウト時のDNSラウンドロビンと簡易的な負荷分散
Composeでscaleオプションや--scaleフラグを使って同一サービスを複数コンテナに増やした場合、内蔵DNSサーバーは少し気の利いた振る舞いをしてくれます。同じサービス名に対して複数のAレコードを返すようになるのです。
たとえばwebというサービスを3つにスケールした状態で、appコンテナからwebという名前で名前解決を行うと、127.0.0.11から返ってくる応答には3つのIPアドレスが含まれます。appコンテナ側のDNSリゾルバ(多くはglibcやmuslに含まれるリゾルバ)は、これらのIPアドレスを順番に試しながら接続を試みるため、結果として3つのwebコンテナにリクエストが分散されていきます。これが、DNSラウンドロビンと呼ばれる簡易的な負荷分散のプロセスです。
DNSラウンドロビンは「簡易的」な仕組みです。本番環境では専用のロードバランサーに任せる方が、結果的に再現性を保てます。
ただし、DNSラウンドロビンは本格的なロードバランサーと比較すると、いくつかの制約があります。
- リクエストが均等に分散される保証がない(リゾルバ側のキャッシュや試行順序に依存する)
- 異常なコンテナを検知して自動的に外すヘルスチェック機能がない
- コンテナがダウンした直後でも、一定時間は古いIPが返り続ける可能性がある
ローカル開発や動作確認では十分に便利な仕組みですが、本番環境で可用性を確保したい場合は、NginxやHAProxy、あるいはKubernetesのServiceリソースなど、専用のオーケストレーターに任せる方が確実です。Composeのスケールは、あくまで開発時の挙動確認や負荷検証の入口として捉えるのが、無理のない付き合い方だと感じています。
外部ドメインへの問い合わせとフォールバックのプロセス
最後に、内蔵DNSサーバーがカバーできないケースについても触れておきましょう。コンテナ内からインターネット上のドメイン(たとえば外部のAPIエンドポイントやパッケージレジストリ)を引く必要があるとき、127.0.0.11は該当するレコードを持っていないため、そのまま応答することができません。
このような場合、内蔵DNSサーバーは外部のDNSサーバーに問い合わせをフォールバックします。具体的には、ホストマシンに設定されたDNSサーバー、またはdocker-compose.ymlのdnsオプションで明示的に指定したサーバーが利用されます。
dnsオプションを指定するときは、docker-compose.ymlのservices配下にdnsというキーを配列で記述します。たとえば公開DNSである8.8.8.8(Google Public DNS)や1.1.1.1(Cloudflare)を指定したい場合は、dns: ["8.8.8.8", "1.1.1.1"]のように追記します。この設定を行うと、コンテナ内の/etc/resolv.confにこれらのアドレスが書き込まれ、外部ドメインの名前解決をComposeレベルで制御できます。プロキシ環境下で開発しているときや、企業ネットワークで外部DNSを固定したいときには、このdnsオプションが頼りになります。
ここで一つ、先回りした補足です。dnsオプションを指定しても、内蔵DNSサーバー(127.0.0.11)が完全にバイパスされるわけではありません。/etc/resolv.confには127.0.0.11と指定したdnsオプションのアドレスが併記される形になり、リゾルバは順番に問い合わせを試みます。多くのケースではCompose上のサービス間通信が127.0.0.11で先に解決されるため、期待どおりの挙動になりますが、外部DNSに到達できない構成(VPN配下や社内プロキシ環境など)では順番が影響することもあります。もし思わぬ挙動に遭遇したときは、docker compose exec <サービス> cat /etc/resolv.confで実際の順番を確認してみてください。
まとめ:ネットワークを意識することが再現性への第一歩
ここまで、Docker Composeの内部ネットワークと名前解決の仕組みを、5つのプロセスで整理してきました。
docker compose upでプロジェクト専用ネットワークが自動生成される- コンテナ内の
/etc/resolv.confに127.0.0.11が書き込まれ、内蔵DNSサーバーが利用される - 内蔵DNSサーバーはDockerデーモン側にあり、コンテナの起動・停止に合わせて動的にAレコードを管理する
- スケール時は複数のAレコードを返すことで、DNSラウンドロビンによる簡易的な負荷分散が実現する
- 内蔵DNSサーバーが解決できない外部ドメインは、ホストやdnsオプションの指定先へフォールバックされる
Composeが私たちに提供してくれているのは「ネットワークを明示的に書かなくても、サービス名で疎通できる」というシンプルさです。そのシンプルさの裏側には、上記のような複数のプロセスが静かに協調して動く仕組みがあります。
ローカル開発で「動いているけれど、その理由までは追っていない」状態から一歩進んで、ネットワークと名前解決のプロセスを意識し始めると、コンテナが期待どおりに動かないときの切り分けがぐっと早くなります。たとえば「appからdbに繋がらない」という事象に遭遇したとき、それがComposeのDNSの問題なのか、デフォルトブリッジに取り残されているのか、ファイアウォールなのかを、順番に確かめていけるようになります。
次のステップとしては、docker-compose.ymlの中でnetworks:セクションを明示的に書いて、サービスごとにネットワークを分離してみるのがおすすめです。サービス間の不要な通信を断ち切ることで、セキュリティと再現性の両方を高めることができますし、今回の内容で触れた「ユーザー定義ネットワーク」への理解も、より具体的な手応えとして身につくはずです。まずは手元のdocker-compose.ymlにnetworks:ブロックを一行追加して、その動きをdocker network inspectで覗いてみてください。思っていたよりもずっとシンプルな構造に、きっと安心感を覚えていただけるはずです。