1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker 入門者のアウトプットAdvent Calendar 2024

Day 7

とりあえず Docker compose で Web + DB やってみる

Last updated at Posted at 2024-12-05

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 に書くとこんな感じ。

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 はホスト (外ネットワーク) につなげないとはいえ、セキュアな部分はそんな雑に扱うべきでない
完成品
server.ts
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。パスワード等は環境変数設定から可能。

他、ネットワーク設定など入れてできあがり。

とりあえず動いた、というようなコードです。もっと良い書き方があるかもしれません。

compose.yaml
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 ネットワーク周りをある程度勉強してからでないと、自分が何をしているかすら分からなくなってしまう恐れがあります
    • 結果 networkshostname の設定だけでできるのが分かりましたが、それにたどり着くまで紆余曲折・・・どんな道を辿ったかも忘れちまった
  • 再ビルドし忘れに注意 (2024/12/08 追記)
    • compose.yamlbuild: があれば、Dockerfile を探してビルドしてくれます
    • しかし、ビルドして既に同名のイメージが存在する場合、ビルドせずそのイメージを起動してしまいます
    • ソースや Dockerfile に編集がなければ何ら問題ありませんが、そうでなく編集がある場合は厄介で、編集が反映されず起動されてしまいます
    • オプションを加え docker compose up --build とすると、毎回必ず Dockerfile のビルドをしてから起動してくれます

おわり

Docker で、ネットワーク周りも仮想的に色々できるのが分かりました。これを仮想でないところを用意してやろうとすると、まあ大変だろうなぁというのは容易に想像がつきますね。

また一つ、Docker のコア機能を知れたと同時に、コンテナ環境の便利さに気付けました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?