FlaskとPostgreSQLのマルチコンテナ環境における永続化・名前解決・標準出力ログの実機検証
概要
本記事では、Python学習累計108時間の現役トラックドライバーが、Docker Composeを用いたFlask(Webアプリケーション)とPostgreSQL(データベース)のマルチコンテナ環境 において、以下3点を実機検証した結果を事実ベースで報告します。
- ①データ永続化(Volume)の挙動
- ②コンテナ間のネットワーク名前解決の挙動
- ③
loggingモジュールを用いたエラーハンドリングの設計
外部API(Gemini API)連携時における環境変数の未設定エラーを検知し、トラブルシューティングを行うプロセスを通じて、コンテナ開発におけるログ運用の重要性についても検証します。
📦 1. コンテナのライフサイクルとデータ永続化の実機検証
この章で分かること:
docker compose downでコンテナを消しても、DBのデータが本当に消えないのか?を実機で確かめます。
Docker Composeでコンテナを構築する際、docker compose downでコンテナを削除すると、コンテナ内部のデータは同時に失われます。しかし、sales_data_appではPostgreSQLのデータを保持する目的で、名前付きボリューム(Named Volume) を以下のように定義しています。
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
コンテナ内部の/var/lib/postgresql/dataを、コンテナのライフサイクルから独立したpostgres_dataという領域に接続することで、コンテナを再作成してもデータが失われない構成になっています。
🔍 実機検証:down → up でデータは残るか
まず、ボリュームが実際に生成されているかをホスト側から確認しました。
$ docker volume ls
DRIVER VOLUME NAME
local sales_data_app_postgres_data
次に、既存データの件数を確認した上で、コンテナを意図的に削除・再作成し、データが保持されるかを検証しました。
| 手順 | コマンド | 結果 |
|---|---|---|
| ① 削除前のレコード数を確認 | docker compose exec db psql -U postgres -d sales_db -c "SELECT COUNT(*) FROM daily_sales;" |
5件 |
| ② コンテナを削除 | docker compose down |
- |
| ③ コンテナを再作成 | docker compose up -d |
- |
| ④ 削除後のレコード数を再確認 | docker compose exec db psql -U postgres -d sales_db -c "SELECT COUNT(*) FROM daily_sales;" |
5件(変化なし) |
✅ 結果に対する考察
コンテナを一度破棄し、新規に作成し直した後も、レコード数は5件のまま変化しませんでした。この結果から、docker compose downはコンテナという実行環境(消耗品)を削除する操作であり、volumesで明示的に切り出されたデータ領域(資産)はその影響を受けないことが実機ベースで確認できました。
言い換えると、コンテナは「使い捨て可能な実行単位」として設計し、状態を持たせたいデータは必ず名前付きボリュームやバインドマウントとして外部化しておく、というコンテナ設計における基本原則を、破棄・再作成という実際の操作を通じて検証した形になります。
🌐 2. コンテナ間通信における名前解決の仕組みと実機検証
この章で分かること:
webコンテナはなぜlocalhostではなくdbという名前でPostgreSQLに繋がるのか?を実機で確かめます。
Docker Compose環境 において、別々のコンテナ同士が通信を行う際、接続先にlocalhostを指定すると通信エラーが発生します。これは、各コンテナにおけるlocalhostがコンテナ自身の内部ネットワークを指すためです。
Docker Compose は起動時に独自のブリッジネットワークを自動的に構築し、docker-compose.ymlで定義された「サービス名」をホスト名としてIPアドレスに自動翻訳する(名前解決)機能を提供します。これにより、コンテナ再作成に伴い内部IPアドレスが変動した場合でも、コード側を変更することなく通信を維持できます。実際、sales_data_appの環境変数でも接続先はdb:5432とサービス名で指定されており、IPアドレスを直接記述していません。
🔍 実機検証:サービス名は本当にIPへ変換されるのか
実際にネットワークが機能しているかを確かめるため、起動中のwebコンテナ内部からシステムライブラリを用いて、サービス名dbに対応するIPアドレスの問い合わせを行いました。
# webコンテナのbashシェルを実行
$ docker compose exec web bash
# Pythonのsocketライブラリを用いて'db'の名前解決を実行
root@1d708ec39a0e:/app# python -c "import socket; print(socket.gethostbyname('db'))"
出力結果
172.18.0.2
✅ 結果に対する考察
コンテナ内部のLinux環境において、文字列dbが172.18.0.2という具体的なIPアドレスに正しく名前解決されている事実を確認しました。1章で確認したボリュームがコンテナの再作成に依存しないのと同様、このIPアドレスもコンテナの再作成によって変動しうる値です。だからこそ、接続先設定にはIPアドレスではなくサービス名を指定することで、環境に依存しない確実な通信が確立できることが証明されました。
📝 3. ログ設計と実装(標準出力への統合)
この章で分かること:コンテナのログを「ファイル」ではなく「標準出力」に集約すべき理由と、その実装方法です。
コンテナ環境におけるアプリケーションのログ運用では、ログをコンテナ内のファイルに永続化するのではなく、「標準出力(stdout)および標準エラー出力(stderr)に集約する」 ことが推奨されます。これにより、プラットフォームやDocker自体がログを一元管理・収集することが可能になります。
Pythonの標準ライブラリであるloggingモジュールを導入し、アプリケーションのライフサイクルおよびエラー発生時のスタックトレースを正確に捕捉する設計を行いました。
アプリケーションコード(app.py 抜粋)
import os
import logging
from flask import Flask, render_template, request, jsonify
from models import db
import config
# 標準出力へのログフォーマットおよび出力レベルの設定
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = config.SQLALCHEMY_DATABASE_URI
db.init_app(app)
# データベース初期化プロセスのログ記録
with app.app_context():
try:
db.create_all()
logger.info("Database tables initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize database tables: {e}")
def _generate_ai_advice(ranked_sales):
if not ranked_sales:
logger.warning("AI advice requested but ranked_sales is empty.")
return "データがありません。"
try:
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
logger.error("GEMINI_API_KEY is missing from environment variables.")
return "設定エラー"
logger.info(f"Requesting Gemini AI advice for products: {len(ranked_sales)} items.")
# (APIリクエスト処理を実行)
logger.info("Gemini AI advice generated successfully.")
return response.text
except Exception as e:
# exc_info=True により、エラー発生箇所のスタックトレースを詳細に出力
logger.error("Unexpected error during AI advice generation", exc_info=True)
return "一時的なエラーが発生しました。"
🔧 4. 実際のログデータに基づくトラブルシューティングのプロセス
この章で分かること:実際に発生したAPIエラーを、ログだけを手がかりに特定・修正するまでの一部始終です。
実際に上記システムを起動し、ブラウザ経由で外部API連携機能を実行した際に、ターミナルへ出力された実際のコンテナログおよび対応手順は以下の通りです。
出力されたエラーログ(事実)
web-1 | 2026-07-01 11:23:53 [INFO] Generating AI daily greeting...
web-1 | 2026-07-01 11:23:54 [ERROR] Failed to generate AI greeting: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'API key not valid. Please pass a valid API key.'}}
web-1 | Traceback (most recent call last):
web-1 | File "/app/app.py", line 231, in api_greeting
...
google.genai.errors.ClientError: 400 INVALID_ARGUMENT.
原因の特定と対策
| 手順 | 内容 |
|---|---|
| ログの解析 | 例外情報(ClientError: 400 INVALID_ARGUMENT)とメッセージ内のAPI key not validという文言から、GEMINI_API_KEYが不正または未設定であることを特定 |
| 修正の実行 |
docker-compose.ymlが参照する.envの内容を確認し、正しいAPIキーへ書き換え |
| 環境の再適用 |
docker compose down → docker compose up でコンテナを再作成し、設定を反映 |
なお、1章で検証した通り、この再作成操作によってPostgreSQLのデータが失われることはありません。コンテナ(実行環境)とボリューム(データ)が分離されているからこそ、環境変数の修正のようなコンテナ側の変更を、データを失うリスクなく安全に反映できます。
コンテナ再起動後、再度ブラウザから同機能を実行した際のログは以下の通り変化しました。
web-1 | 2026-07-01 11:24:26 [INFO] Requesting Gemini AI advice for products: 5 items.
web-1 | 2026-07-01 11:24:26 [INFO] Gemini AI advice generated successfully.
エラーログから根本原因を論理的に特定し、環境変数の修正を行うことで、想定通りの正常な実行状態(INFO)へ遷移した事実を確認できました。
🚩 結論
マルチコンテナ環境における開発では、ブラックボックス化しやすい以下3点を、それぞれ実機検証と標準出力への適切なログ設計によって可視化することが極めて重要です。
- コンテナの寿命とデータの寿命の違い(1章)
- コンテナ間の通信挙動(2章)
-
内部のエラー状態(3・4章)
感傷や推測を排除し、システムが出力する結果という「事実」をベースにトラブルシューティングを行うアプローチは、アプリケーションの保守性と信頼性を担保するための確実な基盤となります。
sales_data_app 整備記録シリーズ
「本番出走前・車検三部作」
🚚「出走後メンテナンスシリーズ」
🍞