0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker ComposeでLLMアプリの開発環境と本番構成を分離する

0
Last updated at Posted at 2026-09-29

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アプリの継続的な運用につながります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?