Docker Composeでマルチサービス定義

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

Docker Composeでマルチサービス定義

Docker Composeは、複数のコンテナサービスをまとめて定義し、ライフサイクルを管理するためのツールです。アプリケーションがWebサーバー、データベース、キャッシュなど複数のコンポーネントで構成される場合に、それぞれのコンテナ定義、ネットワーク、ボリュームなどを一元的に管理できるため、開発・運用効率が大幅に向上します。

要件と前提

、以下の要件を満たすDocker Composeによるマルチサービス定義と運用について解説します。

  • マルチサービス定義: 複数コンテナ(Webアプリ、データベース)をDocker Composeで定義します。

  • 安全なBashスクリプト: set -euo pipefailtrapmktemp を用いたスクリプト作成。

  • 外部ツール連携: jq を用いたJSON処理、curl によるTLS通信と再試行/バックオフ処理。

  • systemd連携: サービスを systemd のUnit/Timerとして管理し、自動起動・ログ確認を実装します。

  • 権限管理: root 権限の扱いと権限分離に関する注意点を示します。

  • 冪等性: スクリプトや設定が何度実行されても同じ結果になるようにします。

前提として、Docker EngineとDocker Composeが動作するLinux環境(例: Ubuntu Server)が構築済みであることを想定します。

実装

Docker Compose ファイルの準備

基本的なWebアプリケーションとデータベースの構成を例に、docker-compose.yml を作成します。

ディレクトリ構成

.
├── app/
│   ├── Dockerfile
│   └── app.py
├── docker-compose.yml
└── scripts/
    ├── start_app.sh
    ├── health_check.sh
    └── manage_backup.sh

アプリケーションサービス例

app/Dockerfile:

FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

app/requirements.txt:

Flask
redis
psycopg2-binary

app/app.py:

import os
from flask import Flask
import redis
import psycopg2

app = Flask(__name__)

REDIS_HOST = os.environ.get('REDIS_HOST', 'redis')
POSTGRES_HOST = os.environ.get('POSTGRES_HOST', 'db')
POSTGRES_DB = os.environ.get('POSTGRES_DB', 'myapp_db')
POSTGRES_USER = os.environ.get('POSTGRES_USER', 'user')
POSTGRES_PASSWORD = os.environ.get('POSTGRES_PASSWORD', 'password')

@app.route('/')
def hello():
    try:
        r = redis.Redis(host=REDIS_HOST, port=6379, db=0)
        r.ping()
        redis_status = "Redis connected"
    except Exception as e:
        redis_status = f"Redis connection error: {e}"

    try:
        conn = psycopg2.connect(host=POSTGRES_HOST, database=POSTGRES_DB, user=POSTGRES_USER, password=POSTGRES_PASSWORD)
        cur = conn.cursor()
        cur.execute("SELECT 1")
        db_status = "PostgreSQL connected"
        cur.close()
        conn.close()
    except Exception as e:
        db_status = f"PostgreSQL connection error: {e}"

    return f"Hello from Flask! {redis_status}. {db_status}\n"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

docker-compose.yml の例

Docker Composeファイルのバージョンは、Docker Composeドキュメント({{jst_today}}時点)で最新のものを参照してください。

# docker-compose.yml

version: '3.8'

services:
  web:
    build: ./app
    ports:

      - "5000:5000"
    environment:
      FLASK_ENV: development
      REDIS_HOST: redis
      POSTGRES_HOST: db
      POSTGRES_DB: myapp_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    depends_on:

      - db

      - redis
    networks:

      - app_network
    restart: unless-stopped # コンテナ停止時以外は再起動

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: myapp_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:

      - db_data:/var/lib/postgresql/data
    networks:

      - app_network
    healthcheck: # データベースのヘルスチェック
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:6-alpine
    networks:

      - app_network
    healthcheck: # Redisのヘルスチェック
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  db_data:

networks:
  app_network:
    driver: bridge

安全なスクリプトでの起動

start_app.sh:Docker Composeアプリケーションを安全に起動するためのスクリプトです。

#!/bin/bash


# scripts/start_app.sh

# 1. 厳格なエラーハンドリングを有効化 (必須)


#    -e: コマンドが失敗した場合、即座にスクリプトを終了


#    -u: 未定義の変数を使用した場合、エラーとして終了


#    -o pipefail: パイプライン中のコマンドが一つでも失敗した場合、パイプライン全体を失敗とする

set -euo pipefail

# 2. 一時ディレクトリの作成と自動クリーンアップ (必須)


#    mktemp -d: 安全な一時ディレクトリを作成

TMP_DIR=$(mktemp -d -t docker-compose-XXXXXX)

#    trap: スクリプト終了時に一時ディレクトリを削除


#    EXITシグナル: スクリプトが正常終了、エラー終了に関わらず実行される

trap 'echo "Cleaning up temporary directory: ${TMP_DIR}"; rm -rf "${TMP_DIR}"' EXIT

echo "Starting Docker Compose services..."
echo "Temporary directory for logs/data: ${TMP_DIR}"

# 環境変数 DOCKER_COMPOSE_FILE に compose ファイルのパスを設定


# デフォルトはカレントディレクトリの docker-compose.yml

DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-docker-compose.yml}"
PROJECT_NAME="myapp_$(basename "$(pwd)")" # プロジェクト名をディレクトリ名から生成

# 3. Docker Composeの実行 (冪等性を考慮)


#    -f: 使用するComposeファイルを指定


#    --project-name: プロジェクト名を明示的に指定し、コンテナ名の衝突を防ぐ


#    --build: イメージが変更されている場合は再ビルド


#    -d: デタッチドモード(バックグラウンド)で実行

echo "Running: docker compose -f \"${DOCKER_COMPOSE_FILE}\" --project-name \"${PROJECT_NAME}\" up --build -d"
docker compose -f "${DOCKER_COMPOSE_FILE}" --project-name "${PROJECT_NAME}" up --build -d

echo "Waiting for services to be healthy..."

# サービスがhealthyになるまで待機


# --timeout: ヘルスチェックのタイムアウトを設定 (デフォルトは無制限)


# ここでは5分 (300秒) を設定

docker compose -f "${DOCKER_COMPOSE_FILE}" --project-name "${PROJECT_NAME}" ps --services --filter "status=running" | while read -r service; do
    echo "Waiting for service: $service"
    docker compose -f "${DOCKER_COMPOSE_FILE}" --project-name "${PROJECT_NAME}" wait "$service" --timeout 300 || {
        echo "Error: Service $service did not become healthy within 300 seconds." >&2
        exit 1
    }
    echo "Service $service is healthy."
done

echo "Docker Compose services started and healthy."
echo "Application can be accessed at http://localhost:5000"

安全なスクリプトのポイント:

  • set -euo pipefail: スクリプトの堅牢性を高めます。

  • trap '...' EXIT: 予期せぬ終了時でも一時ファイルを確実にクリーンアップします。

  • mktemp -d: 一意で安全な一時ディレクトリを作成し、競合や情報漏洩のリスクを低減します。

  • --project-name: 環境ごとに異なるプロジェクト名を付与することで、複数のComposeプロジェクトが同じホストで実行される際のコンテナ名衝突を防ぎます。

jq と curl の活用例

health_check.sh:Webサービスのエンドポイントを curl で確認し、jq でJSONレスポンスを処理する例です。

#!/bin/bash


# scripts/health_check.sh

set -euo pipefail

APP_URL="http://localhost:5000"
MAX_RETRIES=10
RETRY_DELAY_SEC=5

echo "Checking application health at ${APP_URL}..."

for ((i=1; i<=$MAX_RETRIES; i++)); do
    echo "Attempt $i/$MAX_RETRIES: Fetching health status..."

    # curl の安全な利用例


    # -s: サイレントモード


    # -S: エラー表示 (サイレントモードでも)


    # --fail: HTTPステータスが400以上の場合にエラーコードを返す


    # --max-time: 全体的なタイムアウト


    # --connect-timeout: 接続タイムアウト


    # --retry: 指定回数リトライ (ここではループで制御するため1回)


    # --retry-delay: リトライ間の遅延 (ループで制御するため0)


    # -v / --cacert / --tlsv1.2: TLS関連 (本例ではHTTPなので不要だが、HTTPSの場合に利用)


    # --output /dev/null: 出力を捨ててステータスコードのみ確認する場合


    # --write-out '%{http_code}' /dev/stderr: HTTPステータスコードを標準エラーに出力


    # -L: リダイレクトをフォロー

    RESPONSE=$(curl -sS --fail -L \
                    --max-time 10 --connect-timeout 5 \
                    "${APP_URL}")

    # curl が成功した場合

    if [ $? -eq 0 ]; then
        echo "Received response:"
        echo "$RESPONSE" | jq '.' # jqでJSONを整形して出力

        # jq を使ってレスポンス内容をチェックする例


        # . | contains("Redis connected") で部分文字列のマッチング

        if echo "$RESPONSE" | jq -e 'contains("Redis connected") and contains("PostgreSQL connected")' > /dev/null; then
            echo "Application is healthy and services are connected!"
            exit 0
        else
            echo "Application response indicates a problem with backend services. Retrying..."
        fi
    else
        echo "curl failed (HTTP status or connection error). Retrying..."
    fi

    if [ "$i" -lt "$MAX_RETRIES" ]; then
        echo "Waiting ${RETRY_DELAY_SEC} seconds before next retry..."
        sleep "$RETRY_DELAY_SEC"
    fi
done

echo "Error: Application did not become healthy after $MAX_RETRIES retries." >&2
exit 1

curl のポイント:

  • --fail: HTTP 4xx/5xx レスポンスで curl が終了コードを返すため、スクリプトでエラーを捕捉しやすくなります。

  • -sS: サイレントモードで進捗メータなどを非表示にしつつ、エラーメッセージは表示します。

  • --max-time, --connect-timeout: ネットワークの問題でハングアップするのを防ぎます。

  • HTTPSの場合: --cacert でCA証明書を指定、--tlsv1.2 で特定のTLSバージョンを強制するなど、セキュリティを強化できます。

  • ループと sleep: 指数バックオフは curl の組み込み機能だけでは難しいため、シェルスクリプトのループで実装します。

jq のポイント:

  • jq '.': 受信したJSONを整形して表示します。

  • jq -e 'contains("...")': 特定の文字列が含まれているかをチェックし、結果を終了コードで返します。これにより、シェルスクリプトでの条件判定が容易になります。

  • > /dev/null: jq の出力を捨て、終了コードのみを利用します。

検証

サービス起動確認

start_app.sh スクリプトを実行して、Docker Composeサービスが正常に起動することを確認します。

cd /path/to/your/project
chmod +x scripts/start_app.sh
scripts/start_app.sh

スクリプトが「Docker Compose services started and healthy.」と出力すれば成功です。

Webブラウザで http://localhost:5000 にアクセスし、「Hello from Flask! Redis connected. PostgreSQL connected.」と表示されることを確認します。

ログ確認

docker compose logs コマンドで、実行中のコンテナのログを確認できます。

docker compose -f docker-compose.yml --project-name myapp_projectname logs -f

myapp_projectnamestart_app.sh スクリプトで生成されたプロジェクト名に置き換えてください。-f オプションでリアルタイムにログを追跡できます。

運用

systemd との連携

Docker Composeアプリケーションをサーバー起動時に自動で立ち上げ、安定稼働させるために systemd と連携させます。

フローチャート

graph TD
    A["システム起動/タイマー発動"] --> B{"systemdユニット実行"};
    B --> C["ExecStart: docker compose up -d"];
    C --> D["サービス起動"];
    D -- 稼働中 --> E["systemd status/journalctl"];
    D -- 停止時 --> F["ExecStop: docker compose down"];
    E -- 定期実行 --> G{"systemdタイマー発動"};
    G --> H["ExecStart: docker compose exec ... script"];

Unitファイル例 (/etc/systemd/system/myapp.service)

アプリケーションサービスを起動・停止するUnitファイルです。 UserGroup を指定することで、Dockerデーモンとの通信権限を持つ非rootユーザーで実行することが推奨されます。

# /etc/systemd/system/myapp.service

[Unit]
Description=My Docker Compose Application Service
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]

# User/Group を指定し、root以外のユーザーで実行する


# Dockerグループに属するユーザーであればdockerデーモンと通信可能

User=your_user # Dockerグループに属するユーザー
Group=docker

# WorkingDirectory を指定

WorkingDirectory=/path/to/your/project

# ExecStart: start_app.sh スクリプトを呼び出す


# Environment: スクリプトに compose ファイルパスを渡す

ExecStart=/bin/bash /path/to/your/project/scripts/start_app.sh

# ExecStop: docker compose down でサービスを停止

ExecStop=/usr/local/bin/docker compose -f /path/to/your/project/docker-compose.yml --project-name myapp_projectname down

# 起動タイプ。forkingはバックグラウンドプロセスがメインになる場合に指定


# compose up -d はバックグラウンドで実行されるため forking が適している

Type=forking

# 再起動ポリシー

Restart=on-failure
RestartSec=5s

# 標準出力/エラー出力のログを journalctl で確認

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

注意点:

  • /path/to/your/project は実際のプロジェクトディレクトリのパスに置き換えてください。

  • your_user は Dockerグループに属する非rootユーザー名に置き換えてください。

  • myapp_projectnamedocker-compose.yml で定義した、または start_app.sh で生成されるプロジェクト名に置き換えてください。

  • ExecStartExecStopdocker compose のパスは、which docker compose で確認したフルパスを使用してください (例: /usr/local/bin/docker compose)。

Timerファイル例 (/etc/systemd/system/myapp-backup.timer と /etc/systemd/system/myapp-backup.service)

定期的なバックアップ処理などのタスクは systemd のTimer機能で実行できます。

myapp-backup.timer:

# /etc/systemd/system/myapp-backup.timer

[Unit]
Description=Run My Docker Compose App Backup every day

[Timer]

# 毎日午前3時に実行

OnCalendar=*-*-* 03:00:00

# タイマーがシステムの起動後すぐに実行されるように (OnCalendarが過去の場合)

Persistent=true

[Install]
WantedBy=timers.target

myapp-backup.service:

# /etc/systemd/system/myapp-backup.service

[Unit]
Description=My Docker Compose App Backup Task

# myapp.service が動作していることを前提とする

After=myapp.service
Requires=myapp.service

[Service]
User=your_user
Group=docker
WorkingDirectory=/path/to/your/project

# データベースコンテナ内でバックアップスクリプトを実行する例

ExecStart=/usr/local/bin/docker compose -f /path/to/your/project/docker-compose.yml --project-name myapp_projectname exec -T db pg_dumpall -U user > /path/to/your/project/backups/db_backup_$(date +\%Y\%m\%d\%H\%M\%S).sql
StandardOutput=journal
StandardError=journal

注意点:

  • バックアップスクリプトは /path/to/your/project/backups ディレクトリを事前に作成しておく必要があります。

  • pg_dumpall のパスはコンテナ内のパスを想定しています。

  • db コンテナで pg_dumpall が利用可能であることを前提としています。

起動/ログ確認

systemd ユニットとタイマーを有効化し、起動とログ確認を行います。

  1. systemd の設定をリロード:

    sudo systemctl daemon-reload
    
  2. サービスを有効化して起動:

    sudo systemctl enable myapp.service
    sudo systemctl start myapp.service
    
  3. タイマーを有効化:

    sudo systemctl enable myapp-backup.timer
    sudo systemctl start myapp-backup.timer
    
  4. サービスの状態確認:

    sudo systemctl status myapp.service
    
  5. ログの確認:

    sudo journalctl -u myapp.service -f
    sudo journalctl -u myapp-backup.service -f
    

権限分離と root 権限の扱い

  • root 権限の最小化: Docker Composeを本番環境で運用する場合、サービス起動スクリプトやsystemdユニットは可能な限り root 以外のユーザーで実行すべきです。Dockerグループに所属する非 root ユーザーを作成し、そのユーザーでDockerデーモンとのやり取りを行わせるのが一般的なプラクティスです。

  • コンテナ内のユーザー: コンテナイメージ内部では root ユーザーではなく、専用の非特権ユーザーでプロセスを実行するようにDockerfileを記述することがセキュリティベストプラクティスです。

  • ボリュームの権限: ボリュームをマウントする場合、ホスト側のディレクトリの権限が適切に設定されているか確認し、コンテナ内のユーザーが必要な読み書き権限を持つようにします。

  • シークレット管理: データベースパスワードなどの機密情報は、docker-compose.yml に直接ハードコードせず、environment セクションの _FILE サフィックスを利用したシークレットファイルやDocker Secrets(Swarmモードの場合)を利用することを検討してください[1]。

トラブルシュート

一般的なDocker Composeのトラブルシューティングは以下の通りです。

  • コンテナが起動しない:

    • docker compose logs <service_name> で特定サービスのログを確認。

    • docker compose ps でコンテナの状態を確認(Exited の場合、終了コードを確認)。

    • docker compose build --no-cache でイメージを再ビルドしてみる。

    • Dockerデーモンが起動しているか sudo systemctl status docker で確認。

  • サービス間の通信ができない:

    • docker-compose.ymlnetworks 定義を確認し、全てのサービスが同じネットワークに接続されているか確認。

    • サービス名がDNSとして解決されているか、コンテナ内で ping <service_name> を試す(例: docker compose exec web ping db)。

    • ファイアウォール(ufw/firewalld)がコンテナ間の通信をブロックしていないか確認。

  • ポートが競合する:

    • docker-compose.ymlports マッピングを確認し、ホスト側のポートが他のプロセスで使用されていないか sudo netstat -tulpn | grep :<port> で確認。
  • ボリュームのマウントエラー:

    • ホスト側のパスが存在し、Dockerデーモンやコンテナを実行するユーザーが必要な権限を持っているか確認。

    • Dockerボリュームの権限が正しく設定されているか確認。

まとめ

本記事では、Docker Composeを使用して複数のサービスを定義し、安全なBashスクリプトによる起動、jqcurlを用いた健全性チェック、そしてsystemdと連携した自動運用までの一連の流れを解説しました。特に、Bashスクリプトの堅牢化 (set -euo pipefail, trap, mktemp) や systemd によるサービス管理は、本番運用において不可欠な要素です。

これらの実践を通じて、Docker Composeアプリケーションの信頼性と運用効率を向上させることができます。セキュリティと権限分離の原則を常に念頭に置き、最小限の権限でサービスを運用するよう努めてください。


[1] Docker Docs: Security best practices, {{jst_today}}更新. https://docs.docker.com/develop/security-best-practices/

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました