プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

docker-composeを使ってみる

dockerしか使ったことない人間がdocker-composeを使います。

動機

Flaskアプリケーション作成のTutorialをやったらコンテナ化したくなりました。

コンテナ化したら今度はnginxコンテナやMySQLコンテナと連携させてくなりました。

連携させたら複数のコンテナをビルド/実行/停止/削除する作業があまりに面倒でdocker-composeに行き着きました。


dockerとの比較

dockerは1つのコンテナを扱います。だからコンテナの数だけdockerコマンドを打つ必要があります。私のケースだとnginxコンテナ、Flaskコンテナ、MySQLコンテナに対して合計3回のdockerコマンドを打ちます。1回のビルド/実行/停止/削除には合計12回コマンド投入が必要です。

加えてコマンドのオプションが多いので1つ1つのコマンドが長いです。何回もビルド/実行/停止/削除を繰り返していると疲弊します。

docker-composeは複数のコンテナを束ねて1つのserviceとして扱います。複数のコンテナをdocker-composeコマンドを1つで操作できます。1回のビルド/実行/停止/削除に必要なコマンド投入回数は4です。

またオプションはYAMLファイルに記載するので1つ1つのコマンドが短いです。それでも何回もビルド/実行/停止/削除を繰り返していると疲弊しますがdockerで戦うより遥かに楽です。


Kubernetesとの比較

これまで「コンテナを組み合わせたらKubernetes」と安直に考えていて、docker-composeの立ち位置がわかりませんでした。ですが今回その有難みを痛感しました。

昨今の開発、デプロイでは小さなアプリケーションでも複数コンテナになります。小さなアプリケーションに対してKubernetesは大きすぎます。

Kubernetesは最大の拡張性と可用性をアプリケーションに提供することを念頭に作られたモンスターです。Kubernetesのパワーを必要としないアプリケーションは沢山あります。そんなときにはdocker-composeの方が便利です。

大規模なシステムをdocker-composeで運用することも可能です。しかし高い拡張性と可用性が求められる場面ではKubernetesの方が構築も運用も楽になるでしょう。


docker-compose.yml

YAMLファイルにサービスを定義します。

nginxコンテナ、Flaskコンテナ、MySQLコンテナで構成されるサービスの記述例です。

version: "3"
services:
  app:
    image: app
    build: ./app
    environment:
      - APP_ENV=docker
    working_dir: /app
    command: ["./wait-for-it.sh", "mydb:3306", "--", "python3", "app.py"]
  proxy:
    image: proxy
    build: ./proxy
    ports:
      - "8080:80"
    depends_on:
      - app
  db:
    image: mysql:latest
    environment:
      - MYSQL_ROOT_PASSWORD=mysecret
      - MYSQL_DATABASE=books
    volumes:
      - mydb-volume:/var/lib/mysql
volumes:
  mydb-volume:
    driver: local
  • version:docker-compose.ymlの仕様版数。本日時点で3.8が最新。
  • services:この中に複数のコンテナを定義する。
  • app、proxy、db:サービス名。各コンテナ定義の箱。
  • build:指定されたフォルダのDockerfileでビルドする。
  • depends_on:起動順序を指定。

残りはdockerの基本的なコマンドがわかればイメージできると思います。


ビルド

docker-compose build

キャッシュを使わない場合は--no-cacheオプションを付けます。キャッシュを使わないでビルドすると過去のイメージがdangling(tagなし)状態になるのでdocker image pruneで掃除しましょう。


実行

docker-compose up -d

ログ確認

docker-compose logs -f

サービス名を指定すればそのサービスのログのみ表示できます。


コンテナに接続

docker-compose exec -it <コンテナ名> /bin/<シェル>

シェルはベースがalpine系ならash、buster等debian系ならbashです。


終了

docker-compose down

コンテナを終了、削除します。networkも削除します。imageとvolumeは残ります。

-vオプションをつけるとvolumeも削除します。


コンテナ間通信

上記のdocker-compose.ymlのようになにもnetworkを指定しないと自動的にbridge networkが作成されて全てのコンテナが接続されます。

各コンテナのサービス名(app、proxy、db)がnetwork-aliasとして自動登録されるので、コンテナ同士はサービス名で通信することができます。network-aliasはdocker container inspectで確認できます。

実験したところコンテナ名でも名前解決できました。よってコンテナ名で通信することもできます。ただしdocker container inspectで確認してもnetwork-aliasとして登録されていません。不思議です。

docker-compose downするとネットワークも削除されます。


コンテナ名

docker-composeを利用するとコンテナ名は自動で<プロジェクト名>_<サービス名>_<連番>になります。

例:docker-compose-study_app_1

環境変数COMPOSE_PROJECT_NAMEを変更するとプロジェクト名部分を変更できます。

名前全体を明示的に変更したい場合はcontainer_nameを使用します。


volume名

同様にvolume名も自動で<プロジェクト名>_<volume名>_<連番>になります。

nameで変更することができます。

name: my-volume

起動順序の管理

順序性は発生しないのがベストですが現実としてはコンテナを起動する順序は重要です。

nginxコンテナとFlaskコンテナの順序性

nginxは起動時にproxy_passで設定されたコンテナ名(proxy_pass https://myapp:5000の「myapp」)が名前解決できるか確認します。その前にFlaskコンテナが起動していないとコンテナ名で名前解決ができずnginxコンテナが終了してしまいます。

FlaskコンテナとMySQLコンテナの順序性

Flaskコンテナは起動するとDBの初期化(db.create_all())を実行します。このときMySQLサービスが利用不可能だとエラーが発生してFlaskコンテナは終了してしまいます。

今回、期待する起動順序

このため今回のサービスは以下の順序で起動させる必要があります。

MySQLコンテナ起動 → MySQLサービスアップ → Flaskコンテナ起動 → nginxコンテナ起動

depends_on

コンテナの起動順序を制御する仕組みとしてdocker-composeにはdepends_onがあります。depends_onは起動を開始する順序を保証してくれます。

下記のようにnginxコンテナをFlaskコンテナにdepends_onすればnginxコンテナをFlaskコンテナより先に起動してくれます。

  proxy:
    <省略>
    depends_on:
      - app

nginxコンテナが名前解決を始めた頃にはFlaskコンテナの名前は名前解決可能な状態になっています。これでnginxコンテナとFlaskコンテナの順序性の課題は解決されます。

残念ながらdepends_onは起動が完了する順序は保証してくれません。MySQLコンテナを先に起動してもMySQLのサービスが立ち上がる前にFlaskコンテナがDB初期化処理を開始してしまう可能性は残ります。

実際に試したところやはりMySQLサービスが起動完了する前にFlaskコンテナあDB初期化処理を開始してしまいFlaskコンテナは終了してしまいました。nginxコンテナとFlaskコンテナの順序性の課題はdepeds_onでは解決できません。

MySQLサービスの起動を待つ方法

docker-composeの仕組みでは難しく他のツールを活用します。dockerizeやwait-for-it.shが有名です。ここでは導入が容易なwait-for-it.shを紹介します。

docker-compose.ymlに以下のように記述すればwait-for-it.shがmydbの状態を監視してmydbが利用可能になった後に「python3 app.py」を実行します。

command: ["./wait-for-it.sh", "mydb:3306", "--", "python3", "app.py"]

wait-for-it.shはGitで公開されています。

https://github.com/vishnubob/wait-for-it

wait-for-it.shはbashが必要です。Flaskコンテナのベースがalpine(ash系)だと苦労します。当初、私はalpineベースのFlaskコンテナを利用していましたが、bashをインストールするとpipやapk addの動きがおかしくなったのでslim-busterベースに替えました。alpineベースのコンテナもMySQL対応で肥大化していたのでslim-busterに変えてもコンテナサイズは変わりませんでした。