こんにちは、JS2IIUです。
チーム開発において、「私の環境では動くのに、他の人の環境では動かない」という問題に遭遇したことはありませんか?この問題は、開発者のローカル環境の差異(OSのバージョン、インストールされているライブラリのバージョン、環境変数の設定など)によって引き起こされます。
特に機械学習プロジェクトでは、PyTorchやTensorFlowなどのライブラリが特定のCUDAバージョンに依存していたり、データ処理用のツールが複雑な依存関係を持っていたりするため、環境構築の難易度が高くなります。
Dockerは、この問題を根本から解決するコンテナ化技術です。本記事では、Dockerの基礎から実践的なdocker-composeの活用まで、機械学習エンジニアの視点で解説していきます。今回もよろしくお願いします。
1. Dockerの基本概念を理解する
1.1 コンテナとは何か
コンテナは、アプリケーションとその実行に必要なすべての依存関係を1つのパッケージにまとめたものです。仮想マシン(VM)と似ていますが、より軽量で高速に起動できます。
仮想マシンとコンテナの違い:
- 仮想マシン:各VMが完全なOSを含むため、起動に時間がかかり、リソースを多く消費します
- コンテナ:ホストOSのカーネルを共有するため、軽量で高速です。秒単位で起動できます
1.2 Dockerの3つの基本要素
- Dockerイメージ:アプリケーションの設計図。読み取り専用のテンプレート
- Dockerコンテナ:イメージから作成された実行可能なインスタンス
- Dockerボリューム:コンテナが削除されてもデータを永続化する仕組み
2. 実践:PythonアプリケーションをDockerコンテナ化する
2.1 シンプルな機械学習APIのコンテナ化
まず、scikit-learnを使ったシンプルな予測APIをコンテナ化してみましょう。
プロジェクト構成:
ml-api/
├── app.py
├── requirements.txt
├── Dockerfile
└── model.pklapp.py(簡単な予測API):
from flask import Flask, request, jsonify
import pickle
import numpy as np
app = Flask(__name__)
# 事前に学習済みのモデルを読み込む
with open('model.pkl', 'rb') as f:
model = pickle.load(f)
@app.route('/predict', methods=['POST'])
def predict():
"""
入力データを受け取り、予測結果を返すエンドポイント
"""
try:
# JSON形式でデータを受け取る
data = request.get_json()
features = np.array(data['features']).reshape(1, -1)
# 予測を実行
prediction = model.predict(features)
return jsonify({
'prediction': prediction.tolist(),
'status': 'success'
})
except Exception as e:
return jsonify({
'error': str(e),
'status': 'error'
}), 400
if __name__ == '__main__':
# 本番環境では0.0.0.0でリッスンする
app.run(host='0.0.0.0', port=5000, debug=False)requirements.txt:
flask==2.3.0
scikit-learn==1.3.0
numpy==1.24.02.2 効率的なDockerfileの書き方
Dockerfileは、イメージをビルドするための設計図です。以下は、ベストプラクティスを反映したDockerfileです。
# ベースイメージには軽量なPythonイメージを使用
FROM python:3.10-slim
# 作業ディレクトリを設定
WORKDIR /app
# 依存関係ファイルを先にコピー(レイヤーキャッシュを活用)
# requirements.txtが変更されない限り、この層は再利用される
COPY requirements.txt .
# 依存関係をインストール
# --no-cache-dirでキャッシュファイルを保存しないことでイメージサイズを削減
RUN pip install --no-cache-dir -r requirements.txt
# アプリケーションコードをコピー
# requirements.txtより後にコピーすることで、コード変更時の再ビルドを高速化
COPY . .
# コンテナが使用するポートを明示
EXPOSE 5000
# 非rootユーザーで実行(セキュリティのベストプラクティス)
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# コンテナ起動時のコマンド
CMD ["python", "app.py"]重要なポイント:
- レイヤーキャッシュの活用:変更頻度の低いファイル(requirements.txt)を先にコピーすることで、ビルド時間を短縮できます
- イメージサイズの最適化:
-slimイメージの使用や--no-cache-dirオプションでサイズを削減 - セキュリティ:非rootユーザーでアプリケーションを実行
2.3 Dockerイメージのビルドと実行
# イメージをビルド
docker build -t ml-api:v1 .
# コンテナを起動
docker run -d -p 5000:5000 --name ml-api-container ml-api:v1
# APIをテスト
curl -X POST http://localhost:5000/predict \
-H "Content-Type: application/json" \
-d '{"features": [5.1, 3.5, 1.4, 0.2]}'3. マルチステージビルドで本番環境を最適化
機械学習プロジェクトでは、開発時にJupyter Notebookやデバッグツールが必要ですが、本番環境では不要です。マルチステージビルドを使えば、開発ツールを含まない軽量な本番イメージを作成できます。
# ステージ1: ビルド環境(開発ツールを含む)
FROM python:3.10 as builder
WORKDIR /app
COPY requirements.txt .
# ビルド時に必要なツールをインストール
RUN pip install --user --no-cache-dir -r requirements.txt
# ステージ2: 本番環境(最小限の構成)
FROM python:3.10-slim
WORKDIR /app
# ビルドステージからインストール済みのパッケージをコピー
COPY --from=builder /root/.local /root/.local
# アプリケーションコードをコピー
COPY app.py .
COPY model.pkl .
# パスを通す
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]このアプローチにより、最終的なイメージサイズを大幅に削減できます(約1GB → 300MB程度)。
4. docker-composeで複数サービスを管理する
実際の機械学習プロジェクトでは、APIサーバー、データベース、キャッシュサーバーなど複数のサービスが連携します。docker-composeを使えば、これらを一括で管理できます。
4.1 機械学習プロジェクトの典型的な構成
以下は、FastAPI + PostgreSQL + Redisを使った構成例です。
プロジェクト構成:
ml-project/
├── api/
│ ├── Dockerfile
│ ├── main.py
│ └── requirements.txt
├── docker-compose.yml
├── .env
└── init.sqldocker-compose.yml:
version: '3.8'
services:
# FastAPIアプリケーション
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
# 環境変数をファイルから読み込む
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
volumes:
# 開発時にコード変更を即座に反映
- ./api:/app
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# PostgreSQLデータベース
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
# データの永続化
- postgres_data:/var/lib/postgresql/data
# 初期化SQLスクリプト
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
# Redisキャッシュ
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
# 名前付きボリュームの定義
volumes:
postgres_data:
redis_data:.env ファイル(機密情報の管理):
POSTGRES_USER=mluser
POSTGRES_PASSWORD=secure_password_here
POSTGRES_DB=mldb4.2 FastAPIアプリケーションの実装
api/main.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import redis
import psycopg2
from psycopg2.extras import RealDictCursor
import json
import os
app = FastAPI(title="ML API with Cache")
# Redisクライアントの初期化
redis_client = redis.from_url(os.getenv('REDIS_URL'))
# データベース接続の取得
def get_db_connection():
"""
PostgreSQLへの接続を取得する
"""
return psycopg2.connect(
os.getenv('DATABASE_URL'),
cursor_factory=RealDictCursor
)
class PredictionRequest(BaseModel):
"""
予測リクエストのスキーマ
"""
user_id: int
features: list[float]
@app.post("/predict")
async def predict(request: PredictionRequest):
"""
予測を実行し、結果をキャッシュとDBに保存
"""
cache_key = f"prediction:{request.user_id}"
# キャッシュを確認
cached_result = redis_client.get(cache_key)
if cached_result:
return {
"prediction": json.loads(cached_result),
"source": "cache"
}
# 予測を実行(ここでは簡略化)
prediction = sum(request.features) / len(request.features)
# 結果をキャッシュに保存(有効期限:1時間)
redis_client.setex(
cache_key,
3600,
json.dumps(prediction)
)
# 結果をデータベースに保存
try:
with get_db_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO predictions (user_id, features, result)
VALUES (%s, %s, %s)
""",
(request.user_id, request.features, prediction)
)
conn.commit()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {
"prediction": prediction,
"source": "computed"
}
@app.get("/health")
async def health_check():
"""
ヘルスチェックエンドポイント
"""
# Redisとデータベースの接続を確認
try:
redis_client.ping()
with get_db_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1")
return {"status": "healthy"}
except Exception as e:
return {"status": "unhealthy", "error": str(e)}4.3 docker-composeの基本操作
# すべてのサービスを起動
docker-compose up -d
# ログを確認
docker-compose logs -f api
# 特定のサービスを再起動
docker-compose restart api
# すべてのサービスを停止・削除
docker-compose down
# ボリュームも含めて完全に削除
docker-compose down -v5. 開発環境と本番環境の切り替え
docker-composeでは、環境ごとに異なる設定を適用できます。
docker-compose.override.yml(開発環境用):
version: '3.8'
services:
api:
volumes:
# ソースコードをマウント(ホットリロード)
- ./api:/app
environment:
- DEBUG=1
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reloaddocker-compose.prod.yml(本番環境用):
version: '3.8'
services:
api:
# 本番環境ではコードをマウントしない
volumes: []
environment:
- DEBUG=0
# 複数のワーカーで起動
command: gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000使い分け:
# 開発環境(自動的にoverride.ymlが適用される)
docker-compose up -d
# 本番環境
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d6. チーム開発での運用Tips
6.1 Makefileでコマンドを簡略化
複雑なdockerコマンドをMakefileにまとめると、チームメンバーが使いやすくなります。
Makefile:
.PHONY: build up down logs test clean
build:
docker-compose build
up:
docker-compose up -d
down:
docker-compose down
logs:
docker-compose logs -f api
test:
docker-compose exec api pytest tests/
clean:
docker-compose down -v
docker system prune -f使い方:
make build # イメージをビルド
make up # サービスを起動
make logs # ログを表示6.2 よくあるトラブルシューティング
問題1:ポートがすでに使用されている
# 使用中のポートを確認
lsof -i :5000
# docker-compose.ymlでポート番号を変更
ports:
- "5001:5000"問題2:ボリュームのパーミッションエラー
# Dockerfile内でユーザーIDを指定
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN groupadd -g ${GROUP_ID} appuser && \
useradd -u ${USER_ID} -g appuser -m appuser問題3:イメージのビルドキャッシュが原因で古いコードが実行される
# キャッシュを使わずにビルド
docker-compose build --no-cacheまとめ
本記事では、Dockerとdocker-composeを使った開発環境の統一について解説しました。重要なポイントをまとめます:
- Dockerの基本:コンテナは軽量で高速、環境の差異を解消できる
- Dockerfileのベストプラクティス:レイヤーキャッシュの活用、マルチステージビルド、セキュリティ対策
- docker-composeの活用:複数サービスの一括管理、環境変数管理、ボリュームによるデータ永続化
- 環境の切り替え:開発環境と本番環境で異なる設定を適用
機械学習プロジェクトでは、依存関係が複雑になりがちですが、Dockerを活用することで「私の環境では動くのに」問題から解放されます。チーム全体で同じ環境を共有できるため、デバッグやトラブルシューティングの時間を大幅に削減できます。
次のステップとして、KubernetesやDocker Swarmを使ったコンテナオーケストレーション、CI/CDパイプラインへの統合などに挑戦してみてください。
最後まで読んでいただきありがとうございました。


コメント