FlowingDev

Docker Compose入門:コンテナのための万能リモコン

Docker ComposeがYAMLファイルを使って、いかにしてマルチコンテナアプリを管理するのかを学びましょう。複雑なdocker runコマンドを、共有可能な単一の設計図に変える方法を解説します。

ツールを試す: Docker Run to Compose

一言で言うと

Docker Composeは、長ったらしいdocker runコマンドをいくつも並べる代わりに、人間が読める単一のYAMLファイルを使って、複数のコンテナで構成されるアプリケーションを定義し、実行するためのツールだ。

こいつが解決してくれる問題

はじめにdocker runありき。それは…まあ、悪くはなかった。コンテナを1つ動かすだけなら、それで十分。ポートマッピングやボリュームを指定してコマンドを打ち込めば、はい、サービスが起動する。

でも、君のアプリが1つのサービスだけじゃなかったら?Webフロントエンド、バックエンドAPI、PostgreSQLデータベース、そしてRedisキャッシュが必要だとしたら?そうなると、もはや1つのコマンドを叩くだけでは済まない。4つものコマンドをジャグリングするハメになる。

コンテナ同士が通信できるように手動でネットワークを作らなきゃいけない。うっかり全部のコンテナを同じホストポートに割り当てないように気をつけないといけない。各サービス用の、あの長ったらしいdocker runコマンドを、環境変数やボリュームマウントも全部含めて、正確に覚えておかなきゃいけない。この設定を新しいチームメイトと共有したければ、巨大なREADME.mdを書いて、彼らがすべてを正しくコピペしてくれるよう祈るしかない。これはもう、カオスとタイプミス、そして古典的な「まあ、こっちのマシンでは動くんだけどね!」問題への片道切符だ。

このカオスを鎮めるために設計されたのが、Dockerが2014年に買収したFigというプロジェクトから生まれたDocker Composeだ。個々のコンテナという「ペット」を管理するやり方から、アプリケーション全体という「群れ」を統率するやり方へと移行させるためのものなんだ。一つ一つ命令的なコマンド(これを実行して、次にアレを実行して、こう繋いで…)を出す代わりに、宣言的なファイルを1つ書く(俺のアプリの構成はこれだ)。あとはComposeが、それを実現するための面倒な詳細をすべて引き受けてくれる。

裏側の仕組み

Docker Composeの核心は「翻訳機」だ。記述的なYAMLファイルを、Docker Engineへの一連のAPIコールに変換してくれる。コンテナ管理における「言うな、見せろ」の実践版ってわけだ。

docker runのフラグからYAMLのキーへ

たとえば、PostgreSQLデータベースを起動したいとしよう。docker runコマンドは、ハッカー映画に出てくる呪文みたいに見えるかもしれない。

docker run --name my-postgres-db \
  -e POSTGRES_PASSWORD=supersecret \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  --restart unless-stopped \
  -d postgres:14

このコマンドには、名前、パスワード用の環境変数、ポートマッピング、データを永続化するための名前付きボリューム、再起動ポリシーが含まれていて、バックグラウンドで実行される(-d)。

さて、これをdocker-compose.ymlファイルでDocker Compose風に書くとこうなる。

version: '3.8' # Composeファイルのバージョンを指定

services:
  db: # サービスに付ける分かりやすい名前
    image: postgres:14
    container_name: my-postgres-db
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: supersecret
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: # ここで名前付きボリュームを宣言する

対応関係が見えるかい?コマンドラインのフラグが、きれいに整理されたYAMLのキーになっている。ただ見た目が良いだけじゃない。バージョン管理ができて、共有しやすく、エラーもずっと起こしにくい、サービスの定義になっているんだ。

docker run フラグ docker-compose.yml キー 何をするか
postgres:14 (イメージ) image: postgres:14 使用するコンテナイメージを指定する。
--name my-postgres-db container_name: my-postgres-db コンテナに特定の名前を付ける。
-p 5432:5432 ports: ["5432:5432"] ホストからコンテナへポートをマッピングする。
-v pgdata:/var/lib/... volumes: [pgdata:/var/lib/...] 永続データのためにボリュームをマウントする。
-e VAR=value environment: { VAR: value } コンテナ内で環境変数を設定する。
--restart unless-stopped restart: unless-stopped コンテナの再起動ポリシーを定義する。

ネットワークの魔法

ここがComposeの真骨頂だ。さっきのYAMLファイルに、Webアプリのサービスを追加してみよう。こんな感じに。

services:
  db:
    # ... (上で設定したpostgresの設定)
  
  api:
    image: my-cool-api:latest
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://user:supersecret@db:5432/mydatabase
    depends_on:
      - db

docker-compose upを実行すると、Composeは素晴らしいことをやってくれる。君のアプリケーション専用の仮想ネットワークを自動的に作成するんだ。そしてdbとapiの両方のコンテナを、そのネットワークに接続する。

これで、apiサービスはホスト名dbを使ってデータベースに接続できる!これはYAMLファイルでサービスに付けた名前と全く同じだ。もうIPアドレスを調べたり、脆い--linkフラグを使ったりする必要はない。Composeがプライベートネットワーク内のDNS解決をすべて担ってくれる。こうして君のWebアプリコンテナは、データベースコンテナと安全かつ確実に、甘いささやき(というか、まあ、SQLクエリとか)を交わすことができるわけだ。

Composeのライフサイクル

docker-composeコマンドは、アプリケーションスタック全体を操作するためのコントロールパネルだ。

  • docker-compose up: これがメインのコマンド。docker-compose.ymlを読み込み、システムの現状を確認し、ファイルの内容と一致させるために必要なことを何でもやってくれる。ネットワークを作り、イメージをビルドし(buildディレクティブがあれば)、ボリュームを作成し、すべてのサービスコンテナを起動する。もう一度実行すれば、変更点を賢くチェックして、更新が必要なコンテナだけを再作成してくれる。
  • docker-compose down: upの親切な反対コマンド。すべてのコンテナを停止・削除し、デフォルトでは作成したネットワークも削除する。「後片付けよろしく」ボタンだ。完全にまっさらな状態にしたければ、docker-compose down -vで名前付きボリュームも削除できる。
  • docker-compose ps: Composeファイルで管理されている全コンテナのステータスを表示する。
  • docker-compose logs -f: 全サービスのログを、サービスごとに色分けされた便利なストリームで追いかけることができる。デバッグには欠かせない。

実録!あるある話

「こっちのマシンでは動くんだけど」事件

Devonは会社の分析ダッシュボードの新機能を完成させた。これはNode.jsのバックエンドとMongoDBデータベースを使っている。自分のラップトップでは完璧に動作する。コードをプッシュして、新人のSamにプルして実行するように伝えた。DevonのREADMEには、2つの巨大なdocker runコマンドが書かれている。人間であるSamは、データベースのボリュームパスでタイプミスをしてしまった。アプリは起動するが、何も保存できない。Samはバグがアプリのコードにあると思い込み、3時間もデバッグに費やす一方で、Devonは問題ないと言い張る。最終的に、画面共有をしてタイプミスを発見。フラストレーションは最高潮に。

教訓: もしdocker-compose.ymlを使っていたら、オンボーディングはdocker-compose up一発で済んでいたはずだ。ポート、ボリューム、環境変数といった環境全体が全く同じになり、設定ミスという一大ジャンルを根絶し、半日かかったデバッグを30秒の起動時間に変えてくれただろう。

CI/CDパイプラインの悪夢

あるDevOpsチームが、マイクロサービスアプリをテストするためのCIパイプラインを構築していた。アプリにはWebサーバー、APIゲートウェイ、ユーザーサービス、そしてRedisキャッシュがあった。彼らのJenkinsfileはシェルコマンドだらけの恐ろしいスクリプトで、4つのdocker runコールと、APIが接続を試みる前にデータベースが準備できていることを祈るためのsleep 30コマンドが大量にあった。テストは不安定(flaky)で、通ることもあれば、sleepが足りずに失敗することもあった。

教訓: 彼らはパイプラインをDocker Composeを使うように書き直した。Jenkinsfileはdocker-compose -f docker-compose.ci.yml up -dというシンプルなものになった。そしてdepends_onとcondition: service_healthyを使って、データベースが内部のヘルスチェックをパスした後にのみAPIが起動するようにした。不安定だったテストは一夜にして姿を消した。Composeは、必要なテスト環境を毎回、正確に定義し起動するための、信頼性の高い宣言的な方法を提供してくれたのだ。

忘れ去られたサイドカー

開発者のMariaは、メインのPythonアプリケーションをDockerコンテナでデプロイした。数週間後、マネージャーから詳細なパフォーマンス監視を追加するよう頼まれた。最善の方法は、Prometheusエクスポーターを「サイドカー」として実行することだった。これはメインアプリの隣に住み、ネットワークを共有してメトリクスを収集する2つ目のコンテナだ。Mariaは別のdocker runコマンドでそれを設定した。しかし今や、何か変更を加えるたびに2つのコンテナを開始、停止、再起動するのを覚えておかなければならなくなった。ある日、彼女はアプリは再起動したがサイドカーを忘れ、丸一日分のメトリクスを失ってしまった。

教訓: Mariaは設定をdocker-compose.ymlにリファクタリングした。Pythonアプリを1つのサービス、Prometheusエクスポーターを2つ目のサービスとして定義したのだ。今ではdocker-compose upで両方が立ち上がり、docker-compose downで一緒に引退する。本来あるべき姿の、単一の論理ユニットとして管理されるようになった。Composeはサイドカーのようなパターンを、いとも簡単に管理できるようにしてくれる。

よくある間違いや落とし穴

  • depends_onが準備完了まで待ってくれるという勘違い。 デフォルトでは、depends_onは依存コンテナが起動するのを待つだけで、その中のアプリケーションが完全に初期化され、接続を受け付ける準備が整うまで待ってはくれない。APIがデータベースのクエリを受け付ける準備が整う前に起動してしまい、クラッシュする可能性がある。真の解決策は、データベースサービスの定義にhealthcheck命令を使い、condition: service_healthy付きの長いdepends_on構文を使うことだ。
  • 秘密情報をGitにコミットしてしまう。 docker-compose.ymlにPOSTGRES_PASSWORD: my-secret-passwordとハードコードしたくなる誘惑はものすごく強い。やめておけ。リポジトリにアクセスできる人なら誰でも、君のデータベースのパスワードを手に入れてしまう。正しいやり方は、.envファイルを使い(これは.gitignoreに追加する)、Composeファイル内で${POSTGRES_PASSWORD}のように変数を参照することだ。
  • バインドマウントと名前付きボリュームの混同。 .や/で始まるパス(例: ./my-app:/app)はバインドマウントで、ホストマシン上のディレクトリをマッピングする。これはコードの変更が即座に反映されるので、開発には最適だ。単なる名前で始まるパス(例: db-data:/var/lib/mysql)は、Dockerが管理する名前付きボリュームだ。これはデータベースのようなデータを永続化するのにより適している。プラットフォームに依存せず、Dockerエンジンによって管理されるからだ。間違った方を使うと、パーミッションの問題や、思ったほどポータブルでないデータにつながる可能性がある。
  • 大規模な本番環境で使ってしまう。 Docker Composeは開発、テスト、そして小規模な単一ホストの本番デプロイにさえも素晴らしいツールだ。しかし、KubernetesやDocker Swarmのような本格的なコンテナオーケストレーターの代替にはならない。マルチノードクラスタ、自己修復、ノード間のロードバランシング、洗練されたローリングアップデートといった概念が組み込まれていない。その限界を知っておこう。

なぜ知っておくべきか

君のアプリケーションが複数の部品で構成されるようになった瞬間、Docker Composeのことを思い浮かべるべきだ。

  • Webアプリと、それとは別のデータベースを構築している?Composeを使おう。
  • アプリ、キャッシュ、そしてバックグラウンドワーカーを動かす必要がある?Composeを使おう。
  • チームメイトに、開発スタック全体をコマンド一発で実行する方法を提供したい?Composeを使おう。
  • テスト用の一時的な環境を立ち上げる必要があるCIパイプラインをセットアップしている?Composeを使おう。

これは、ローカルでのコンテナベース開発における、業界標準のデフォルトツールだ。docker runの次の一歩として自然なステップであり、より複雑なアプリケーションオーケストレーションの世界への入り口でもある。単一のコンテナを動かすことと、本格的な分散システムを動かすことの間のギャップを埋めてくれる存在なのだ。

もっと深く知る

  • Docker Docs: Compose file version 3 reference - 利用可能なすべてのキーと構文に関する、公式の仕様書であり信頼できる情報源。
  • Docker Docs: Overview of Docker Compose - 開発元自身によるハイレベルな紹介。
  • YAML Spec - すべてのdocker-compose.ymlファイルはYAMLなので、その構文(マップ、リスト、アンカー)を理解しておくと非常に役立つ。
  • Awesome Docker - Dockerに関するリソースのキュレーションリスト。様々なスタック向けのチュートリアルやdocker-compose.ymlのサンプルが多数含まれている。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: Docker Run to Compose