<p><!--META
{
"title": "Docker Composeでマルチサービス定義",
"primary_category": "DevOps",
"secondary_categories": ["Docker", "Linux"],
"tags": ["Docker Compose", "systemd", "jq", "curl", "Bash Scripting", "Containerization"],
"summary": "Docker Composeで複数のサービスを定義し、安全なBashスクリプト、systemd連携、トラブルシューティングを含む運用方法を解説します。",
"mermaid": true,
"verify_level": "L0",
"tweet_hint": {"text":"Docker Composeでマルチサービス定義を安全かつ効率的に運用するためのガイド。systemd連携、安全なBashスクリプト、トラブルシューティングまで網羅。#Docker
#DevOps","hashtags":["#Docker","#DevOps"]},
"link_hints": ["https://docs.docker.com/compose/", "https://docs.docker.com/compose/compose-file/", "https://docs.docker.com/config/daemon/systemd/"]
}
-->
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">Docker Composeでマルチサービス定義</h1>
<p>Docker Composeは、複数のコンテナサービスをまとめて定義し、ライフサイクルを管理するためのツールです。アプリケーションがWebサーバー、データベース、キャッシュなど複数のコンポーネントで構成される場合に、それぞれのコンテナ定義、ネットワーク、ボリュームなどを一元的に管理できるため、開発・運用効率が大幅に向上します。</p>
<h2 class="wp-block-heading">要件と前提</h2>
<p>、以下の要件を満たすDocker Composeによるマルチサービス定義と運用について解説します。</p>
<ul class="wp-block-list">
<li><p><strong>マルチサービス定義</strong>: 複数コンテナ(Webアプリ、データベース)をDocker Composeで定義します。</p></li>
<li><p><strong>安全なBashスクリプト</strong>: <code>set -euo pipefail</code>、<code>trap</code>、<code>mktemp</code> を用いたスクリプト作成。</p></li>
<li><p><strong>外部ツール連携</strong>: <code>jq</code> を用いたJSON処理、<code>curl</code> によるTLS通信と再試行/バックオフ処理。</p></li>
<li><p><strong>systemd連携</strong>: サービスを <code>systemd</code> のUnit/Timerとして管理し、自動起動・ログ確認を実装します。</p></li>
<li><p><strong>権限管理</strong>: <code>root</code> 権限の扱いと権限分離に関する注意点を示します。</p></li>
<li><p><strong>冪等性</strong>: スクリプトや設定が何度実行されても同じ結果になるようにします。</p></li>
</ul>
<p>前提として、Docker EngineとDocker Composeが動作するLinux環境(例: Ubuntu Server)が構築済みであることを想定します。</p>
<h2 class="wp-block-heading">実装</h2>
<h3 class="wp-block-heading">Docker Compose ファイルの準備</h3>
<p>基本的なWebアプリケーションとデータベースの構成を例に、<code>docker-compose.yml</code> を作成します。</p>
<h4 class="wp-block-heading">ディレクトリ構成</h4>
<pre data-enlighter-language="generic">.
├── app/
│ ├── Dockerfile
│ └── app.py
├── docker-compose.yml
└── scripts/
├── start_app.sh
├── health_check.sh
└── manage_backup.sh
</pre>
<h4 class="wp-block-heading">アプリケーションサービス例</h4>
<p><code>app/Dockerfile</code>:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">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"]
</pre>
</div>
<p><code>app/requirements.txt</code>:</p>
<pre data-enlighter-language="generic">Flask
redis
psycopg2-binary
</pre>
<p><code>app/app.py</code>:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">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)
</pre>
</div>
<h4 class="wp-block-heading"><code>docker-compose.yml</code> の例</h4>
<p>Docker Composeファイルのバージョンは、<a href="https://docs.docker.com/compose/compose-file/">Docker Composeドキュメント</a>({{jst_today}}時点)で最新のものを参照してください。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 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
</pre>
</div>
<h3 class="wp-block-heading">安全なスクリプトでの起動</h3>
<p><code>start_app.sh</code>:Docker Composeアプリケーションを安全に起動するためのスクリプトです。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/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"
</pre>
</div>
<p><strong>安全なスクリプトのポイント</strong>:</p>
<ul class="wp-block-list">
<li><p><code>set -euo pipefail</code>: スクリプトの堅牢性を高めます。</p></li>
<li><p><code>trap '...' EXIT</code>: 予期せぬ終了時でも一時ファイルを確実にクリーンアップします。</p></li>
<li><p><code>mktemp -d</code>: 一意で安全な一時ディレクトリを作成し、競合や情報漏洩のリスクを低減します。</p></li>
<li><p><code>--project-name</code>: 環境ごとに異なるプロジェクト名を付与することで、複数のComposeプロジェクトが同じホストで実行される際のコンテナ名衝突を防ぎます。</p></li>
</ul>
<h3 class="wp-block-heading"><code>jq</code> と <code>curl</code> の活用例</h3>
<p><code>health_check.sh</code>:Webサービスのエンドポイントを <code>curl</code> で確認し、<code>jq</code> でJSONレスポンスを処理する例です。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/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
</pre>
</div>
<p><strong><code>curl</code> のポイント</strong>:</p>
<ul class="wp-block-list">
<li><p><code>--fail</code>: HTTP 4xx/5xx レスポンスで <code>curl</code> が終了コードを返すため、スクリプトでエラーを捕捉しやすくなります。</p></li>
<li><p><code>-sS</code>: サイレントモードで進捗メータなどを非表示にしつつ、エラーメッセージは表示します。</p></li>
<li><p><code>--max-time</code>, <code>--connect-timeout</code>: ネットワークの問題でハングアップするのを防ぎます。</p></li>
<li><p>HTTPSの場合: <code>--cacert</code> でCA証明書を指定、<code>--tlsv1.2</code> で特定のTLSバージョンを強制するなど、セキュリティを強化できます。</p></li>
<li><p>ループと <code>sleep</code>: 指数バックオフは <code>curl</code> の組み込み機能だけでは難しいため、シェルスクリプトのループで実装します。</p></li>
</ul>
<p><strong><code>jq</code> のポイント</strong>:</p>
<ul class="wp-block-list">
<li><p><code>jq '.'</code>: 受信したJSONを整形して表示します。</p></li>
<li><p><code>jq -e 'contains("...")'</code>: 特定の文字列が含まれているかをチェックし、結果を終了コードで返します。これにより、シェルスクリプトでの条件判定が容易になります。</p></li>
<li><p><code>> /dev/null</code>: <code>jq</code> の出力を捨て、終了コードのみを利用します。</p></li>
</ul>
<h2 class="wp-block-heading">検証</h2>
<h3 class="wp-block-heading">サービス起動確認</h3>
<p><code>start_app.sh</code> スクリプトを実行して、Docker Composeサービスが正常に起動することを確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">cd /path/to/your/project
chmod +x scripts/start_app.sh
scripts/start_app.sh
</pre>
</div>
<p>スクリプトが「Docker Compose services started and healthy.」と出力すれば成功です。</p>
<p>Webブラウザで <code>http://localhost:5000</code> にアクセスし、「Hello from Flask! Redis connected. PostgreSQL connected.」と表示されることを確認します。</p>
<h3 class="wp-block-heading">ログ確認</h3>
<p><code>docker compose logs</code> コマンドで、実行中のコンテナのログを確認できます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">docker compose -f docker-compose.yml --project-name myapp_projectname logs -f
</pre>
</div>
<p><code>myapp_projectname</code> は <code>start_app.sh</code> スクリプトで生成されたプロジェクト名に置き換えてください。<code>-f</code> オプションでリアルタイムにログを追跡できます。</p>
<h2 class="wp-block-heading">運用</h2>
<h3 class="wp-block-heading"><code>systemd</code> との連携</h3>
<p>Docker Composeアプリケーションをサーバー起動時に自動で立ち上げ、安定稼働させるために <code>systemd</code> と連携させます。</p>
<h4 class="wp-block-heading">フローチャート</h4>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
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"];
</pre></div>
<h4 class="wp-block-heading">Unitファイル例 (<code>/etc/systemd/system/myapp.service</code>)</h4>
<p>アプリケーションサービスを起動・停止するUnitファイルです。
<code>User</code> と <code>Group</code> を指定することで、Dockerデーモンとの通信権限を持つ非rootユーザーで実行することが推奨されます。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /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
</pre>
</div>
<p><strong>注意点</strong>:</p>
<ul class="wp-block-list">
<li><p><code>/path/to/your/project</code> は実際のプロジェクトディレクトリのパスに置き換えてください。</p></li>
<li><p><code>your_user</code> は Dockerグループに属する非rootユーザー名に置き換えてください。</p></li>
<li><p><code>myapp_projectname</code> は <code>docker-compose.yml</code> で定義した、または <code>start_app.sh</code> で生成されるプロジェクト名に置き換えてください。</p></li>
<li><p><code>ExecStart</code> と <code>ExecStop</code> の <code>docker compose</code> のパスは、<code>which docker compose</code> で確認したフルパスを使用してください (例: <code>/usr/local/bin/docker compose</code>)。</p></li>
</ul>
<h4 class="wp-block-heading">Timerファイル例 (<code>/etc/systemd/system/myapp-backup.timer</code> と <code>/etc/systemd/system/myapp-backup.service</code>)</h4>
<p>定期的なバックアップ処理などのタスクは <code>systemd</code> のTimer機能で実行できます。</p>
<p><code>myapp-backup.timer</code>:</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /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
</pre>
</div>
<p><code>myapp-backup.service</code>:</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /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
</pre>
</div>
<p><strong>注意点</strong>:</p>
<ul class="wp-block-list">
<li><p>バックアップスクリプトは <code>/path/to/your/project/backups</code> ディレクトリを事前に作成しておく必要があります。</p></li>
<li><p><code>pg_dumpall</code> のパスはコンテナ内のパスを想定しています。</p></li>
<li><p><code>db</code> コンテナで <code>pg_dumpall</code> が利用可能であることを前提としています。</p></li>
</ul>
<h4 class="wp-block-heading">起動/ログ確認</h4>
<p><code>systemd</code> ユニットとタイマーを有効化し、起動とログ確認を行います。</p>
<ol class="wp-block-list">
<li><p><code>systemd</code> の設定をリロード:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl daemon-reload
</pre>
</div></li>
<li><p>サービスを有効化して起動:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl enable myapp.service
sudo systemctl start myapp.service
</pre>
</div></li>
<li><p>タイマーを有効化:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl enable myapp-backup.timer
sudo systemctl start myapp-backup.timer
</pre>
</div></li>
<li><p>サービスの状態確認:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl status myapp.service
</pre>
</div></li>
<li><p>ログの確認:</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo journalctl -u myapp.service -f
sudo journalctl -u myapp-backup.service -f
</pre>
</div></li>
</ol>
<h3 class="wp-block-heading">権限分離と <code>root</code> 権限の扱い</h3>
<ul class="wp-block-list">
<li><p><strong><code>root</code> 権限の最小化</strong>: Docker Composeを本番環境で運用する場合、サービス起動スクリプトや<code>systemd</code>ユニットは可能な限り <code>root</code> 以外のユーザーで実行すべきです。Dockerグループに所属する非 <code>root</code> ユーザーを作成し、そのユーザーでDockerデーモンとのやり取りを行わせるのが一般的なプラクティスです。</p></li>
<li><p><strong>コンテナ内のユーザー</strong>: コンテナイメージ内部では <code>root</code> ユーザーではなく、専用の非特権ユーザーでプロセスを実行するようにDockerfileを記述することがセキュリティベストプラクティスです。</p></li>
<li><p><strong>ボリュームの権限</strong>: ボリュームをマウントする場合、ホスト側のディレクトリの権限が適切に設定されているか確認し、コンテナ内のユーザーが必要な読み書き権限を持つようにします。</p></li>
<li><p><strong>シークレット管理</strong>: データベースパスワードなどの機密情報は、<code>docker-compose.yml</code> に直接ハードコードせず、<code>environment</code> セクションの <code>_FILE</code> サフィックスを利用したシークレットファイルやDocker Secrets(Swarmモードの場合)を利用することを検討してください[1]。</p></li>
</ul>
<h2 class="wp-block-heading">トラブルシュート</h2>
<p>一般的なDocker Composeのトラブルシューティングは以下の通りです。</p>
<ul class="wp-block-list">
<li><p><strong>コンテナが起動しない</strong>:</p>
<ul>
<li><p><code>docker compose logs <service_name></code> で特定サービスのログを確認。</p></li>
<li><p><code>docker compose ps</code> でコンテナの状態を確認(<code>Exited</code> の場合、終了コードを確認)。</p></li>
<li><p><code>docker compose build --no-cache</code> でイメージを再ビルドしてみる。</p></li>
<li><p>Dockerデーモンが起動しているか <code>sudo systemctl status docker</code> で確認。</p></li>
</ul></li>
<li><p><strong>サービス間の通信ができない</strong>:</p>
<ul>
<li><p><code>docker-compose.yml</code> の <code>networks</code> 定義を確認し、全てのサービスが同じネットワークに接続されているか確認。</p></li>
<li><p>サービス名がDNSとして解決されているか、コンテナ内で <code>ping <service_name></code> を試す(例: <code>docker compose exec web ping db</code>)。</p></li>
<li><p>ファイアウォール(ufw/firewalld)がコンテナ間の通信をブロックしていないか確認。</p></li>
</ul></li>
<li><p><strong>ポートが競合する</strong>:</p>
<ul>
<li><code>docker-compose.yml</code> の <code>ports</code> マッピングを確認し、ホスト側のポートが他のプロセスで使用されていないか <code>sudo netstat -tulpn | grep :<port></code> で確認。</li>
</ul></li>
<li><p><strong>ボリュームのマウントエラー</strong>:</p>
<ul>
<li><p>ホスト側のパスが存在し、Dockerデーモンやコンテナを実行するユーザーが必要な権限を持っているか確認。</p></li>
<li><p>Dockerボリュームの権限が正しく設定されているか確認。</p></li>
</ul></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>本記事では、Docker Composeを使用して複数のサービスを定義し、安全なBashスクリプトによる起動、<code>jq</code>と<code>curl</code>を用いた健全性チェック、そして<code>systemd</code>と連携した自動運用までの一連の流れを解説しました。特に、Bashスクリプトの堅牢化 (<code>set -euo pipefail</code>, <code>trap</code>, <code>mktemp</code>) や <code>systemd</code> によるサービス管理は、本番運用において不可欠な要素です。</p>
<p>これらの実践を通じて、Docker Composeアプリケーションの信頼性と運用効率を向上させることができます。セキュリティと権限分離の原則を常に念頭に置き、最小限の権限でサービスを運用するよう努めてください。</p>
<hr/>
<p>[1] Docker Docs: Security best practices, {{jst_today}}更新. <a href="https://docs.docker.com/develop/security-best-practices/">https://docs.docker.com/develop/security-best-practices/</a></p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
Docker Composeでマルチサービス定義
Docker Composeは、複数のコンテナサービスをまとめて定義し、ライフサイクルを管理するためのツールです。アプリケーションがWebサーバー、データベース、キャッシュなど複数のコンポーネントで構成される場合に、それぞれのコンテナ定義、ネットワーク、ボリュームなどを一元的に管理できるため、開発・運用効率が大幅に向上します。
要件と前提
、以下の要件を満たすDocker Composeによるマルチサービス定義と運用について解説します。
マルチサービス定義: 複数コンテナ(Webアプリ、データベース)をDocker Composeで定義します。
安全なBashスクリプト: set -euo pipefail、trap、mktemp を用いたスクリプト作成。
外部ツール連携: 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_projectname は start_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ファイルです。
User と Group を指定することで、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_projectname は docker-compose.yml で定義した、または start_app.sh で生成されるプロジェクト名に置き換えてください。
ExecStart と ExecStop の docker 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 ユニットとタイマーを有効化し、起動とログ確認を行います。
systemd の設定をリロード:
sudo systemctl daemon-reload
サービスを有効化して起動:
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
タイマーを有効化:
sudo systemctl enable myapp-backup.timer
sudo systemctl start myapp-backup.timer
サービスの状態確認:
sudo systemctl status myapp.service
ログの確認:
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.yml の networks 定義を確認し、全てのサービスが同じネットワークに接続されているか確認。
サービス名がDNSとして解決されているか、コンテナ内で ping <service_name> を試す(例: docker compose exec web ping db)。
ファイアウォール(ufw/firewalld)がコンテナ間の通信をブロックしていないか確認。
ポートが競合する:
docker-compose.yml の ports マッピングを確認し、ホスト側のポートが他のプロセスで使用されていないか sudo netstat -tulpn | grep :<port> で確認。
ボリュームのマウントエラー:
まとめ
本記事では、Docker Composeを使用して複数のサービスを定義し、安全なBashスクリプトによる起動、jqとcurlを用いた健全性チェック、そして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/
コメント