docker compose とは
(雑説明)
複数の Docker イメージを起動して、ネットワーク接続して、1つのアプリがやっと動く、みたいなことは珍しくない構成です。(というかそれが一般的)
そんな「このコンテナとこのコンテナを起動して、こうやってネットワークをつなぐ」を手打ちでやると、起動も停止も大変面倒なのは想像がつきます。
それをファイルにまとめたら、サッとみんな起動・停止できる、それが docker compose です。
インストール
とりあえずコマンドを呼んでみよう。
docker compose --help
# docker: 'compose' is not a docker command.
# See 'docker --help'
ん?無い?
どうやらインストールしないといけないようです。公式によるインストール方法マニュアルはこちら。
僕は rootless で入れているので、apt とかを使う方法だと多分効きません。以前別記事で buildx のインストールをしたときと同じ要領で、マニュアルインストールでいきます。
パッケージマネージャで自動アップデートされないので、rootless モードである、実験用途であるなどの事情がなければ、推奨されない方法です。
Docker の GitHub リポジトリに行き、Releases を確認。
記事執筆時点で v2.31.0 が最新。ご自身の OS、アーキテクチャに合ったバイナリをダウンロードします。僕は Linux の x86_64 です。
ダウンロードしたバイナリを、所定の場所に配置して実行権限を付与し、準備完了。
(以下は Linux の場合)
mv <ダウンロードしたファイル> ~/.docker/cli-plugins/docker-compose
chmod +x .docker/cli-plugins/docker-compose
再び --help
。
docker compose --help
#
# Usage: docker compose [OPTIONS] COMMAND
#
# Define and run multi-container applications with Docker
#
# ...
help が出ました。おk。
まず単品コンテナで
これまた別記事でやった自作 WebApp の立ち上げを、compose に書いてみます。
単品でのビルドおよびコンテナ立ち上げのコマンドは以下。
docker build -t mozjpeg-webui .
docker run --rm -it -p 8000:8000 mozjpeg-webui
compose.yaml
に書くとこんな感じ。
services:
mozjpeg-webui:
build: .
ports:
- 8000:8000
compose は、デフォルトでファイル名 compose.yaml
or docker-compose.yml
を探し実行します。拡張子は .yml
でも .yaml
でも OK。
公式チュートリアルでは、compose.yaml
が推奨のようです。(以前はたしか docker-compose.yml
だった)
立ち上げ。(カレントは compose.yaml
がある場所で)
docker compose up
# [+] Building 0.0s (0/1)
# [+] Running 0/0
# [+] Building 0.3s (22/22) FINISHED
# ...
# Attaching to mozjpeg-webui-1
# mozjpeg-webui-1 | INFO: Will watch for changes in these directories: ['/home/mozjpeg-webui/src']
# mozjpeg-webui-1 | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
# mozjpeg-webui-1 | INFO: Started reloader process [1] using StatReload
# mozjpeg-webui-1 | INFO: Started server process [8]
# mozjpeg-webui-1 | INFO: Waiting for application startup.
# mozjpeg-webui-1 | INFO: Application startup complete.
compose.yaml
で指定したイメージがビルド (build) またはプル (pull) され、イメージからコンテナが作成 (create) されます。
(上では、Buildx のキャッシュのおかげで、一瞬でビルド完了しています。)
FastAPI を使った WebApp なので、FastAPI のログが出ています。http://localhost:8000
にアクセスすると、無事 WebAPP にアクセスできました。
Ctrl + C
でコンテナをシャットダウン。
コンテナは残った状態で停止します。残ったコンテナを消すには以下のコマンド。
docker compose rm
# ? Going to remove mozjpeg-webui-mozjpeg-webui-1 (y/N)
y を入力して削除実行。
# ? Going to remove mozjpeg-webui-mozjpeg-webui-1 Yes
# [+] Removing 1/0
# ✔ Container mozjpeg-webui-mozjpeg-webui-1 Removed
本題 - Web コンテナと DB コンテナの連動
上のように 1つのコンテナの起動を規定するのにも使えますが、ここからが真骨頂。複数のコンテナを動かしてみます。
WebAPP でよくある形式、Web サーバとデータベースサーバのあの形をやってみます。
仕様
以下のようなエンドポイントを持つ WebAPI を作ります。
-
/set/<id>
-
<id>
のフラグをセット (登録) する -
<id>
は数字、以降も同様
-
-
/reset/<id>
-
<id>
のフラグをリセット (解除) する
-
-
/get/<id>
-
<id>
のフラグがセットかリセットかを返してくれる
-
よく例として ToDo リストがあったりしますが、こいつはもっと単純な例です。HTML も絡みません。
Web サーバ実装
データベースは PostgreSQL を使います。(選定理由は、MySQL は触ったことがあるが、PostgreSQL は無い、というだけ。)
Web サーバのランタイムは Deno を使います。Deno 公式ドキュメントに、PostgreSQL にアクセスするサンプルがあるので、有り難く参考にさせてもらいます。
今回、Deno はメインでないので軽い実装に。リクエスト body は使わず URL パスパラメータのみ、レスポンスは JSON でも HTML でもなくプレーンテキストです。
簡単のため、かなり雑に作ったコードです。以下の問題を抱えているので、このコードを何も考えず使用するのは大変危険です。
- SQL インジェクション対策がされていない
queryArray(`INSERT INTO ids VALUES (${id_to_set});`)
- ↑の
${id_to_set}
入力をコントロールすると、任意の SQL を実行できてしまう
- 数字入力のみ受け付けるところ、そのチェックがされていない
- 入力次第で、本来クライアントエラーであるところがサーバエラーとなります
- コードにパスワード直書き
- 本当なら環境変数を使うとかの配慮をすべき
- DB はホスト (外ネットワーク) につなげないとはいえ、セキュアな部分はそんな雑に扱うべきでない
完成品
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
// Database init
const client = new Client({
// hostname: "localhost", // コンテナ上でなくローカル上なら localhost
hostname: "postgres",
port: 5432,
user: "user",
database: "test",
password: "P05T6RE5",
tls: { enabled: false }
});
await client.connect();
await client.queryArray("CREATE TABLE ids (id integer);")
// Server process
Deno.serve(async request => {
const url = new URL(request.url);
// ID set
if(url.pathname.startsWith("/set")) {
const id_to_set = url.pathname.split("/", 3)[2]; // '/set/<id_to_set>'
await client.queryArray(`INSERT INTO ids VALUES (${id_to_set});`);
return new Response(`Set OK - ID ${id_to_set}`, { status: 200 });
}
// ID reset
if(url.pathname.startsWith("/reset")) {
const id_to_reset = url.pathname.split("/", 3)[2]; // '/set/<id_to_reset>'
await client.queryArray(`DELETE FROM ids WHERE id = ${id_to_reset}`);
return new Response(`Reset OK - ID ${id_to_reset}`, { status: 200 });
}
// Get status of specified ID
if(url.pathname.startsWith("/get")) {
const id_to_get= url.pathname.split("/", 3)[2]; // '/get/<id_to_get>'
const result = client.queryArray(`SELECT id FROM ids WHERE id = ${id_to_get}`);
if((await result).rowCount !== 0) {
return new Response(`ID ${id_to_get} is set`, { status: 200 });
} else {
return new Response(`ID ${id_to_get} is reset`, { status: 200 });
}
}
// Otherwise
return new Response("Not found", { status: 404 });
});
Web サーバ Dockerfile
Deno イメージをベースに、サーバのソースをコピー。あとは起動するだけ。
FROM denoland/deno
COPY ./server.ts server.ts
CMD ["run", "--allow-net", "--allow-env", "server.ts"]
compose.yaml
実装
Web サーバのイメージは、上の Dockerfile
から構築するよう指示。
データベースのイメージは、Docker 公式で提供されている postgres
を使うだけで OK。パスワード等は環境変数設定から可能。
他、ネットワーク設定など入れてできあがり。
とりあえず動いた、というようなコードです。もっと良い書き方があるかもしれません。
services:
web:
build: .
depends_on:
db: # ここは Docker ドキュメント丸パクリ
condition: service_healthy
restart: true
ports:
- 8000:8000
networks:
- web-db-connection
db:
image: postgres
healthcheck: # ここも Docker ドキュメントほぼ丸パクリ
test: ["CMD-SHELL", "pg_isready -U user -d test"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=P05T6RE5
- POSTGRES_DB=test
networks:
- web-db-connection
hostname: postgres
networks:
web-db-connection:
# オプション無しで動いた
動作チェック
アプリ起動。
docker compose up
curl
で API にアクセスして確かめます。
curl http://localhost:8000/get/1
# ID 1 is reset
curl http://localhost:8000/set/1
# Set OK - ID 1
curl http://localhost:8000/get/1
# ID 1 is set
curl http://localhost:8000/reset/1
# Reset OK - ID 1
curl http://localhost:8000/get/1
# ID 1 is reset
curl http://localhost:8000/
# Not found
注意点
成功例だけまとめるとアッサリですが、裏では色々試行錯誤した点があってコッテリです。
- DB が完全に立ち上がってから Web を立ち上げないといけない (起動順に注意)
- コンテナが立ち上がっただけでは不完全で、DB 接続受付が開始されたかチェックしないといけない
- 幸運なことに、Docker 公式ドキュメントに PostgreSQL での例が載っていたので、これも丸パクリ
- https://docs.docker.com/compose/how-tos/startup-order/
- コンテナ間通信のため
networks
やらhostname
やらを定義しないといけない- 最初、その辺の設定無しで動かし、もちろんダメでした (Web サーバで ConnectionRefused 発生)
- Docker ネットワーク周りをある程度勉強してからでないと、自分が何をしているかすら分からなくなってしまう恐れがあります
- 結果
networks
とhostname
の設定だけでできるのが分かりましたが、それにたどり着くまで紆余曲折・・・どんな道を辿ったかも忘れちまった
- 再ビルドし忘れに注意 (2024/12/08 追記)
-
compose.yaml
にbuild:
があれば、Dockerfile
を探してビルドしてくれます - しかし、ビルドして既に同名のイメージが存在する場合、ビルドせずそのイメージを起動してしまいます
- ソースや
Dockerfile
に編集がなければ何ら問題ありませんが、そうでなく編集がある場合は厄介で、編集が反映されず起動されてしまいます - オプションを加え
docker compose up --build
とすると、毎回必ずDockerfile
のビルドをしてから起動してくれます
-
おわり
Docker で、ネットワーク周りも仮想的に色々できるのが分かりました。これを仮想でないところを用意してやろうとすると、まあ大変だろうなぁというのは容易に想像がつきますね。
また一つ、Docker のコア機能を知れたと同時に、コンテナ環境の便利さに気付けました。