初めに
初めてDockerを触った時は「ローカルでは動くのになぜかDockerでは動かない」という謎のエラーに何時間も頭を抱え、結局は単純な設定ミスだった…という苦い経験を何度もしてきました。当時はエラーメッセージを見ても何を言われているのか分からず、ただただ途方に暮れていたことを今でも覚えています。
この記事では、そんな「新人時代の私」と同じような悩みを持つ方に向けて、バックエンド開発(FastAPI, Python, PostgreSQLなど)のデプロイ・実行サイクルに沿って、初心者がハマりがちなポイントを4つのフェーズに分けて体系的に解説していきたいと思います。
TL;DR
-
環境定義とネットワーク:コンテナ同士がどう通信するか
-
アプリケーション初期化:コードが動き出す前のインポートの罠
-
サービス起動タイミングの同期:「起動」と「準備完了」の時間差
-
実行時のデータ健全性:動的な環境でのデータ処理の堅牢性
環境定義とネットワーク
Docker Composeを起動すると、プロジェクト専用の仮想ネットワーク(ブリッジネットワーク)が自動的に作成されます。このネットワークの最大の特徴は、Docker独自の内部DNSサーバーが動作している点です。
このフェーズで出てきそうなエラーメッセージ:
Compose構文とDNSエラー
socket.gaierror: [Errno -2] Name or service not known
# または
psycopg2.OperationalError: could not connect to server: Connection refused (localhost:5432)
このエラーを理解する前、まず一緒にこのフェーズの概念モデルを見ていきたいと思います。
概念モデル:ネットワーク構造とDNS
- 正しい名前解決と通信フロー
- 接続エラーになるフロー
上の図は、Docker Composeによって構築されるネットワークの物理的なイメージです。重要なポイントは以下の3点です。
-
ホストとコンテナの境界
ブラウザからアクセスする localhost:8000 は「ホスト(PC)からコンテナへ」の入り口です。しかし、一度コンテナの中に入ると、そこは隔離された別世界になります。 -
内部DNSの役割
backend コンテナが db という名前で通信しようとすると、Docker内部のDNSサーバーが自動的に db コンテナのプライベートIPを探し出して接続を仲介します。 -
localhostの隔離
各コンテナは独自のネットワークスタックを持ちます。backend コンテナ内で localhost を指定すると、それは隣の db ではなく「自分自身(backend)」を指してしまいます。
上記の仕組みを正しく理解していないと、以下のような「設定ミスによる接続エラー」が発生してしまいます。
失敗と修正のコード例
- 例①:接続ホスト名の誤り(localhostの罠)
# ❌ コンテナ内から自分自身に接続しようとしてしまう
DATABASE_URL = "postgresql://user:pass@localhost:5432/db"
# ✅ DNSが解決可能な「サービス名」を指定する
DATABASE_URL = "postgresql://user:pass@db:5432/db"
- 例②:ポート番号の勘違い
# ❌ ホスト側に公開したポート(例: 5433)をコンテナ同士の通信で使う
# コンテナ間では、ホスト側への転送ルールは適用されません
DATABASE_URL = "postgresql://user:pass@db:5433/db"
# ✅ コンテナ間では、アプリが実際に動いている「内部ポート」を使う
DATABASE_URL = "postgresql://user:pass@db:5432/db"
- 例③:ネットワーク分離による孤立
# ❌ backend と db が異なるネットワークにあり、通信できない
backend:
networks: [frontend-net]
db:
networks: [backend-net]
# ✅ 同じネットワークに参加させる
backend:
networks: [app-net]
db:
networks: [app-net]
アプリケーション初期化
コンテナが起動し、Pythonコードが読み込まれる段階です。ここでは「インポート」と「実行」の順序を意識する必要があります。
このフェーズで出てきそうなエラーメッセージ:
初期化時のクラッシュ
Traceback (most recent call last):
File "main.py", line 5, in <module>
from app.database import engine
File "/app/database.py", line 12, in <module>
conn = engine.connect()
sqlalchemy.exc.OperationalError: connection refused
概念モデル:インポート時の即時実行
実はPythonの import は、単に定義を読み込むだけでなく、そのモジュールのトップレベルにあるコードを「実行」します。
この性質により、データベース接続や外部APIの呼び出しをトップレベルに書いてしまうと、アプリケーションのメイン処理(FastAPIの起動など)が始まる前の「準備段階(importフェーズ)」で通信試行が発生します。Docker環境ではDBの準備が整っていないことが多いため、ここでエラーが起きるとアプリ本体が動き出す前にプロセスが強制終了してしまいます。
失敗と修正のコード例
- 例①:モジュール読み込み時の即時接続
# ❌ database.py のトップレベルで直接接続
engine = create_engine(URL)
connection = engine.connect() # インポート時に実行され、DB未起動だとクラッシュする
# ✅ 接続を関数化し、実際に必要になるまで遅延させる(Lazy Initialization)
def get_db():
return engine.connect()
- 例②:グローバルスコープでのデータロード
# ❌ 起動時にマスターデータを直接読み込む。DB未準備だとApp全体が起動不能になる
MASTER_DATA = pd.read_sql("SELECT * FROM category", engine)
# ✅ FastAPIのライフサイクルイベント(lifespan / startup)で実行する
@app.on_event("startup")
async def load_master():
app.state.master_data = pd.read_sql("SELECT * FROM category", engine)
- 例③:相対パスによるファイル読み込み
# ❌ 実行環境のパスに依存し、DockerのWORKDIRとズレることがある
with open("./config.json") as f: ...
# ✅ 絶対パスを生成するか、環境変数でパスを定義する
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(BASE_DIR, "config.json")) as f: ...
サービス起動タイミングの同期
コンテナが起動(Up)しても、中のソフトウェア(PostgreSQL等)が通信を受け入れられるようになるまでにはラグがあります。
起動直後の接続拒否
起動直後の接続拒否
psycopg2.OperationalError: connection to server at "db", port 5432 failed: Connection refused
概念モデル:コンテナUp ≠ サービスReady
Dockerコンテナのステータスが起動(Up)になっても、内部のDBサービスが初期化を終えてポートを開放するまでには時間差があります。
失敗と修正のコード例
- 例①:単純な depends_on の過信
# ❌ 失敗:コンテナの起動順序は守るが、DBが「使える状態」になるのは待たない
depends_on: [db]
# ✅ 正解:Healthcheckを使用して、サービスが「健全」になってから起動する
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d db"]
backend:
depends_on:
db: { condition: service_healthy }
- 例②:安易な sleep による解決
# ❌ 失敗:固定秒数待つ(DBの負荷が高い時に結局失敗する「運任せ」の実裝)
time.sleep(10)
# ✅ 正解:リトライロジックを実装し、接続できるまで数回試行する
while retries < 5:
try: connect(); break
except: time.sleep(2); retries += 1
実行時のデータ健全性
アプリは起動しましたが、動的なDocker環境下では取得データの不備によるエラーが発生しやすくなります。
データ処理エラー
KeyError: 'user_id'
# または
ValueError: Cannot perform groupby with no columns
概念モデル:動的な環境と空チェック
コンテナ環境ではDBの初期状態や同期のズレにより、クエリ結果が空(Empty)で返ってくることが頻繁にあります。
失敗と修正のコード例
- 例①:空のDataFrame操作
# ❌ 失敗:必ずデータが存在する前提で加工ロジックを進める
df = fetch_data()
df.groupby("category").sum()
# ✅ 正解:処理の入り口で早期リターン(Early Return)を行う
if df.empty: return []
- 例②:カラムの存在確認
# ❌ 失敗:特定のカラムを直接指定して抽出する(カラムがないとKeyError)
df = df[["required_column"]]
# ✅ 正解:カラムリストを動的にチェックして安全に抽出する
cols = [c for c in ["required_column"] if c in df.columns]
df = df[cols]
その他
- 環境変数の記述ミス
# ❌ リスト形式はスペースや型によって解析エラーが起きやすい
environment:
- DEBUG= true # スペースが原因でエラーになることがある
# ✅ マップ形式(推奨)
environment:
DEBUG: "true"
終わりに
落とし穴は成長のチャンス。これらの落とし穴を振り返ると、右も左もわからず、たった一行の設定ミスで何時間も溶かしていた新人時代の苦労を思い出します。
当時は自分の力不足に落ち込むこともありましたが、今思えばその時に必死で解決した経験の一つひとつが、現在の「Dockerの仕組み」に対する深い理解へと繋がっています。
最後に伝えたいのは、「エラーを恐れないでほしい」ということです。Docker環境でのエラーは、インフラとコードの境界線を学ぶ最高の教材だと思います。
Connection Refusedが出たら「あ、今はDBが準備中なんだな」、KeyErrorが出たら「動的な環境ならではの空データが来たな」と、楽しむくらいの余裕を持って挑戦してください。
たくさん失敗をして、最高に堅牢なバックエンドを構築しましょう。応援しています!