LLMアプリを開発していると、開発中はソースコードをマウントして即時反映したい一方、本番では固定したイメージを使い、不要なポートやデバッグ設定を公開したくありません。
この問題は、1つの compose.yml に開発用・本番用の設定を詰め込むのではなく、共通設定と環境別の override ファイルに分離すると扱いやすくなります。
今回は、FastAPI製のLLMアプリとPostgreSQLを例に、以下の3ファイルで構成します。
.
├── compose.yml
├── compose.dev.yml
├── compose.prod.yml
├── Dockerfile
├── .env.example
└── app/
└── main.py
実際にこの構成で起動確認を行い、開発時はホットリロード、本番時はビルド済みイメージを使う形に整理しました。
共通設定を compose.yml にまとめる
まず、開発・本番のどちらでも必要になるサービスを compose.yml に定義します。
services:
app:
image: ${APP_IMAGE:-llm-app:local}
env_file:
- .env
depends_on:
db:
condition: service_healthy
networks:
- backend
db:
image: postgres:16
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 10
networks:
- backend
volumes:
postgres_data:
networks:
backend:
app と db は同じ backend ネットワークに接続します。アプリからデータベースへ接続するときは、localhost ではなくサービス名の db を指定します。
例えば接続先は次のようになります。
postgresql://app_user:password@db:5432/llm_app
depends_on の condition を使うことで、PostgreSQLのコンテナが起動しただけでなく、ヘルスチェックを通過してからアプリを起動できます。
開発環境ではソースコードをマウントする
開発用の差分は compose.dev.yml に記述します。
services:
app:
build:
context: .
target: development
ports:
- "8000:8000"
volumes:
- ./app:/workspace/app
environment:
APP_ENV: development
RELOAD: "true"
LLM_BASE_URL: ${LLM_BASE_URL}
command: >
uvicorn app.main:app
--host 0.0.0.0
--port 8000
--reload
db:
ports:
- "5432:5432"
開発時だけホストの app ディレクトリをコンテナへマウントします。これにより、ファイルを編集するとUvicornのリロードが動作します。
また、データベースの 5432 ポートも開発時だけ公開しています。ホスト側のDBクライアントやマイグレーションツールから接続できるため便利ですが、本番では外部公開しない方が安全です。
Dockerfileは、開発用ステージと本番用ステージを分けておきます。
FROM python:3.12-slim AS base
WORKDIR /workspace
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
FROM base AS development
RUN pip install --no-cache-dir uvicorn[standard]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
FROM base AS production
RUN useradd --create-home appuser
USER appuser
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
開発用ステージではリロードを有効にし、本番用ステージでは専用ユーザーで実行します。コンテナをrootユーザーのまま動かさないだけでも、事故時の影響を抑えられます。
本番環境ではイメージを固定する
本番用の compose.prod.yml では、ソースコードのマウントやビルド設定を使わず、あらかじめ作成したイメージを指定します。
services:
app:
image: ${APP_IMAGE}
env_file:
- .env.prod
environment:
APP_ENV: production
LOG_LEVEL: info
expose:
- "8000"
restart: unless-stopped
db:
env_file:
- .env.prod
restart: unless-stopped
ports ではなく expose を使っている点がポイントです。expose はCompose内部のネットワークから利用できるポートを示すだけで、ホストの全インターフェースには公開しません。
リバースプロキシやロードバランサーから同じネットワーク経由でアクセスする構成を想定しています。
本番イメージは、デプロイ前にCIなどで作成します。
docker build \
--target production \
-t registry.example.com/llm-app:2026-09-24 .
docker push registry.example.com/llm-app:2026-09-24
.env.prod では、デプロイするイメージタグを指定します。
APP_IMAGE=registry.example.com/llm-app:2026-09-24
POSTGRES_DB=llm_app
POSTGRES_USER=app_user
POSTGRES_PASSWORD=change-me
LLM_BASE_URL=https://llm-gateway.internal
APIキーやデータベースのパスワードをGitリポジトリへコミットしないことも重要です。.env.example には変数名だけを置き、実際の値はデプロイ先のシークレット管理機能や環境変数から注入します。
起動コマンドを使い分ける
共通設定に環境別の差分を重ねる流れは次の通りです。
開発環境は、共通ファイルと開発用ファイルを重ねて起動します。
docker compose \
-f compose.yml \
-f compose.dev.yml \
up --build
バックグラウンドで起動する場合は -d を追加します。
docker compose \
-f compose.yml \
-f compose.dev.yml \
up -d --build
本番環境では、同じ要領で本番用ファイルを指定します。
docker compose \
-f compose.yml \
-f compose.prod.yml \
up -d
実際にデプロイする前には、最終的に適用される設定を確認しておくと安心です。
docker compose \
-f compose.yml \
-f compose.prod.yml \
config
ここで、開発用の volumes、--reload、5432:5432 などが本番設定に残っていないか確認します。
LLMアプリでは、接続先を環境変数にしておくと、開発時はローカルの推論サーバー、本番時は社内ゲートウェイなどへ切り替えられます。
import os
import httpx
from fastapi import FastAPI
app = FastAPI()
LLM_BASE_URL = os.environ["LLM_BASE_URL"]
@app.post("/generate")
async def generate(prompt: str):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{LLM_BASE_URL}/generate",
json={"prompt": prompt},
timeout=60,
)
response.raise_for_status()
return response.json()
コードを変更せず、設定ファイルだけで接続先を切り替えられるため、環境ごとの差分が明確になります。
まとめ
Docker ComposeでLLMアプリを運用する場合は、次のように責務を分けると管理しやすくなります。
-
compose.yml:サービス、ネットワーク、永続ボリュームなどの共通設定 -
compose.dev.yml:ソースコードのマウント、ポート公開、ホットリロード -
compose.prod.yml:固定イメージ、再起動設定、内部公開 -
.env:環境ごとの接続先や設定値 -
Dockerfile:開発用・本番用のビルドステージ
開発の便利さと本番の安全性は、同じ設定ファイルで無理に両立させる必要はありません。Composeのファイルを重ね合わせる仕組みを使い、環境ごとの差分を小さく明示することが、LLMアプリの継続的な運用につながります。